[tbb-commits] [tor-browser] 03/10: Bug 40933: Add tor-launcher functionality

gitolite role git at cupani.torproject.org
Mon Oct 10 20:17:17 UTC 2022


This is an automated email from the git hooks/post-receive script.

richard pushed a commit to branch tor-browser-102.3.0esr-12.0-2
in repository tor-browser.

commit ec870ce688510579033be50020c2c7eefc83f7ff
Author: Pier Angelo Vendrame <pierov at torproject.org>
AuthorDate: Mon Oct 10 15:13:04 2022 +0200

    Bug 40933: Add tor-launcher functionality
---
 browser/installer/package-manifest.in              |   1 +
 toolkit/components/moz.build                       |   1 +
 .../tor-launcher/TorBootstrapRequest.jsm           | 131 ++++
 .../components/tor-launcher/TorLauncherUtil.jsm    | 600 +++++++++++++++++
 .../components/tor-launcher/TorMonitorService.jsm  | 451 +++++++++++++
 toolkit/components/tor-launcher/TorParsers.jsm     | 275 ++++++++
 toolkit/components/tor-launcher/TorProcess.jsm     | 551 +++++++++++++++
 .../components/tor-launcher/TorProtocolService.jsm | 749 +++++++++++++++++++++
 .../components/tor-launcher/TorStartupService.jsm  |  76 +++
 toolkit/components/tor-launcher/components.conf    |  10 +
 toolkit/components/tor-launcher/moz.build          |  17 +
 .../components/tor-launcher/tor-launcher.manifest  |   1 +
 12 files changed, 2863 insertions(+)

diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index ff87c7e39db8..d66e99231802 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -236,6 +236,7 @@
 @RESPATH@/browser/chrome/browser.manifest
 @RESPATH@/chrome/pdfjs.manifest
 @RESPATH@/chrome/pdfjs/*
+ at RESPATH@/components/tor-launcher.manifest
 @RESPATH@/chrome/torbutton.manifest
 @RESPATH@/chrome/torbutton/*
 @RESPATH@/chrome/toolkit at JAREXT@
diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build
index 86a289d2c71d..b405fe52eb79 100644
--- a/toolkit/components/moz.build
+++ b/toolkit/components/moz.build
@@ -75,6 +75,7 @@ DIRS += [
     "thumbnails",
     "timermanager",
     "tooltiptext",
+    "tor-launcher",
     "typeaheadfind",
     "utils",
     "url-classifier",
diff --git a/toolkit/components/tor-launcher/TorBootstrapRequest.jsm b/toolkit/components/tor-launcher/TorBootstrapRequest.jsm
new file mode 100644
index 000000000000..4f0ae240b31d
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorBootstrapRequest.jsm
@@ -0,0 +1,131 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorBootstrapRequest", "TorTopics"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { setTimeout, clearTimeout } = ChromeUtils.import(
+  "resource://gre/modules/Timer.jsm"
+);
+
+const { TorProtocolService } = ChromeUtils.import(
+  "resource://gre/modules/TorProtocolService.jsm"
+);
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://gre/modules/TorLauncherUtil.jsm"
+);
+
+/* tor-launcher observer topics */
+const TorTopics = Object.freeze({
+  BootstrapStatus: "TorBootstrapStatus",
+  BootstrapError: "TorBootstrapError",
+  LogHasWarnOrErr: "TorLogHasWarnOrErr",
+});
+
+// modeled after XMLHttpRequest
+// nicely encapsulates the observer register/unregister logic
+class TorBootstrapRequest {
+  constructor() {
+    // number of ms to wait before we abandon the bootstrap attempt
+    // a value of 0 implies we never wait
+    this.timeout = 0;
+    // callbacks for bootstrap process status updates
+    this.onbootstrapstatus = (progress, status) => {};
+    this.onbootstrapcomplete = () => {};
+    this.onbootstraperror = (message, details) => {};
+
+    // internal resolve() method for bootstrap
+    this._bootstrapPromiseResolve = null;
+    this._bootstrapPromise = null;
+    this._timeoutID = null;
+  }
+
+  async observe(subject, topic, data) {
+    const obj = subject?.wrappedJSObject;
+    switch (topic) {
+      case TorTopics.BootstrapStatus: {
+        const progress = obj.PROGRESS;
+        const status = TorLauncherUtil.getLocalizedBootstrapStatus(obj, "TAG");
+        if (this.onbootstrapstatus) {
+          this.onbootstrapstatus(progress, status);
+        }
+        if (progress === 100) {
+          if (this.onbootstrapcomplete) {
+            this.onbootstrapcomplete();
+          }
+          this._bootstrapPromiseResolve(true);
+          clearTimeout(this._timeoutID);
+        }
+
+        break;
+      }
+      case TorTopics.BootstrapError: {
+        // first stop our bootstrap timeout before handling the error
+        clearTimeout(this._timeoutID);
+
+        await TorProtocolService.stopBootstrap();
+
+        const message = obj.message;
+        const details = obj.details;
+        if (this.onbootstraperror) {
+          this.onbootstraperror(message, details);
+        }
+        this._bootstrapPromiseResolve(false);
+        break;
+      }
+    }
+  }
+
+  // resolves 'true' if bootstrap succeeds, false otherwise
+  bootstrap() {
+    if (this._bootstrapPromise) {
+      return this._bootstrapPromise;
+    }
+
+    this._bootstrapPromise = new Promise((resolve, reject) => {
+      this._bootstrapPromiseResolve = resolve;
+
+      // register ourselves to listen for bootstrap events
+      Services.obs.addObserver(this, TorTopics.BootstrapStatus);
+      Services.obs.addObserver(this, TorTopics.BootstrapError);
+
+      // optionally cancel bootstrap after a given timeout
+      if (this.timeout > 0) {
+        this._timeoutID = setTimeout(async () => {
+          await TorProtocolService.stopBootstrap();
+          if (this.onbootstraperror) {
+            this.onbootstraperror(
+              "Tor Bootstrap process timed out",
+              `Bootstrap attempt abandoned after waiting ${this.timeout} ms`
+            );
+          }
+          this._bootstrapPromiseResolve(false);
+        }, this.timeout);
+      }
+
+      // wait for bootstrapping to begin and maybe handle error
+      TorProtocolService.connect().catch(async err => {
+        clearTimeout(this._timeoutID);
+        // stopBootstrap never throws, at the moment
+        await TorProtocolService.stopBootstrap();
+        if (this.onbootstraperror) {
+          this.onbootstraperror(err.message, "");
+        }
+        this._bootstrapPromiseResolve(false);
+      });
+    }).finally(() => {
+      // and remove ourselves once bootstrap is resolved
+      Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
+      Services.obs.removeObserver(this, TorTopics.BootstrapError);
+    });
+
+    return this._bootstrapPromise;
+  }
+
+  async cancel() {
+    clearTimeout(this._timeoutID);
+
+    await TorProtocolService.stopBootstrap();
+
+    this._bootstrapPromiseResolve(false);
+  }
+}
diff --git a/toolkit/components/tor-launcher/TorLauncherUtil.jsm b/toolkit/components/tor-launcher/TorLauncherUtil.jsm
new file mode 100644
index 000000000000..c2e066e5d29f
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorLauncherUtil.jsm
@@ -0,0 +1,600 @@
+// Copyright (c) 2022, The Tor Project, Inc.
+// See LICENSE for licensing information.
+
+"use strict";
+
+/*************************************************************************
+ * Tor Launcher Util JS Module
+ *************************************************************************/
+
+var EXPORTED_SYMBOLS = ["TorLauncherUtil"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const kPropBundleURI = "chrome://torbutton/locale/torlauncher.properties";
+const kPropNamePrefix = "torlauncher.";
+const kIPCDirPrefName = "extensions.torlauncher.tmp_ipc_dir";
+
+let gStringBundle = null;
+
+class TorFile {
+  // The nsIFile to be returned
+  file = null;
+
+  // A relative or absolute path that will determine file
+  path = null;
+  pathIsRelative = false;
+  // If true, path is ignored
+  useAppDir = false;
+
+  isIPC = false;
+  checkIPCPathLen = true;
+
+  static _isFirstIPCPathRequest = true;
+  static _isUserDataOutsideOfAppDir = undefined;
+  static _dataDir = null;
+  static _appDir = null;
+
+  constructor(aTorFileType, aCreate) {
+    this.fileType = aTorFileType;
+
+    this.getFromPref();
+    this.getIPC();
+    // No preference and no pre-determined IPC path: use a default path.
+    if (!this.file && !this.path) {
+      this.getDefault();
+    }
+
+    if (!this.file && this.path) {
+      this.pathToFile();
+    }
+    if (this.file && !this.file.exists() && !this.isIPC && aCreate) {
+      this.createFile();
+    }
+    this.normalize();
+  }
+
+  getFile() {
+    return this.file;
+  }
+
+  getFromPref() {
+    const prefName = `extensions.torlauncher.${this.fileType}_path`;
+    this.path = Services.prefs.getCharPref(prefName, "");
+    if (this.path) {
+      const re = TorLauncherUtil.isWindows ? /^[A-Za-z]:\\/ : /^\//;
+      this.isRelativePath = !re.test(this.path);
+      // always try to use path if provided in pref
+      this.checkIPCPathLen = false;
+    }
+  }
+
+  getIPC() {
+    const isControlIPC = this.fileType === "control_ipc";
+    const isSOCKSIPC = this.fileType === "socks_ipc";
+    this.isIPC = isControlIPC || isSOCKSIPC;
+
+    const kControlIPCFileName = "control.socket";
+    const kSOCKSIPCFileName = "socks.socket";
+    this.ipcFileName = isControlIPC ? kControlIPCFileName : kSOCKSIPCFileName;
+    this.extraIPCPathLen = this.isSOCKSIPC ? 2 : 0;
+
+    // Do not do anything else if this.path has already been populated with the
+    // _path preference for this file type (or if we are not looking for an IPC
+    // file).
+    if (this.path || !this.isIPC) {
+      return;
+    }
+
+    // If this is the first request for an IPC path during this browser
+    // session, remove the old temporary directory. This helps to keep /tmp
+    // clean if the browser crashes or is killed.
+    if (TorFile._isFirstIPCPathRequest) {
+      TorLauncherUtil.cleanupTempDirectories();
+      TorFile._isFirstIPCPathRequest = false;
+    } else {
+      // FIXME: Do we really need a preference? Or can we save it in a static
+      // member?
+      // Retrieve path for IPC objects (it may have already been determined).
+      const ipcDirPath = Services.prefs.getCharPref(kIPCDirPrefName, "");
+      if (ipcDirPath) {
+        // We have already determined where IPC objects will be placed.
+        this.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        this.file.initWithPath(ipcDirPath);
+        this.file.append(this.ipcFileName);
+        this.checkIPCPathLen = false; // already checked.
+        return;
+      }
+    }
+
+    // If XDG_RUNTIME_DIR is set, use it as the base directory for IPC
+    // objects (e.g., Unix domain sockets) -- assuming it is not too long.
+    const env = Cc["@mozilla.org/process/environment;1"].getService(
+      Ci.nsIEnvironment
+    );
+    if (!env.exists("XDG_RUNTIME_DIR")) {
+      return;
+    }
+    const ipcDir = this.createUniqueIPCDir(env.get("XDG_RUNTIME_DIR"));
+    if (ipcDir) {
+      const f = ipcDir.clone();
+      f.append(this.ipcFileName);
+      if (this.isIPCPathLengthOK(f.path, this.extraIPCPathLen)) {
+        this.file = f;
+        this.checkIPCPathLen = false; // no need to check again.
+
+        // Store directory path so it can be reused for other IPC objects
+        // and so it can be removed during exit.
+        Services.prefs.setCharPref(kIPCDirPrefName, ipcDir.path);
+      } else {
+        // too long; remove the directory that we just created.
+        ipcDir.remove(false);
+      }
+    }
+  }
+
+  // This block is used for the TorBrowser-Data/ case.
+  getDefault() {
+    let torPath = "";
+    let dataDir = "";
+    // FIXME: TOR_BROWSER_DATA_OUTSIDE_APP_DIR is used only on macOS at the
+    // moment. In Linux and Windows it might not work anymore.
+    // We might simplify the code here, if we get rid of this macro.
+    // Also, we allow specifying directly a relative path, for a portable mode.
+    // Anyway, that macro is also available in AppConstants.
+    if (TorFile.isUserDataOutsideOfAppDir) {
+      if (TorLauncherUtil.isMac) {
+        torPath = "Contents/Resources/";
+      }
+      torPath += "TorBrowser/Tor";
+    } else {
+      torPath = "Tor";
+      dataDir = "Data/";
+    }
+
+    switch (this.fileType) {
+      case "tor":
+        if (TorLauncherUtil.isMac) {
+          this.path = `${torPath}/tor`;
+        } else {
+          this.path =
+            torPath + "/tor" + (TorLauncherUtil.isWindows ? ".exe" : "");
+        }
+        break;
+      case "torrc-defaults":
+        this.path = TorFile.isUserDataOutsideOfAppDir
+          ? `${torPath}/torrc-defaults`
+          : `${dataDir}Tor/torrc-defaults`;
+        break;
+      case "torrc":
+        this.path = `${dataDir}Tor/torrc`;
+        break;
+      case "tordatadir":
+        this.path = `${dataDir}Tor`;
+        break;
+      case "toronionauthdir":
+        this.path = `${dataDir}Tor/onion-auth`;
+        break;
+      case "pt-profiles-dir":
+        this.path = TorFile.isUserDataOutsideOfAppDir
+          ? "Tor/PluggableTransports"
+          : `${dataDir}Browser`;
+        break;
+      case "pt-startup-dir":
+        if (TorLauncherUtil.isMac && TorFile.isUserDataOutsideOfAppDir) {
+          this.path = "Contents/MacOS/Tor";
+        } else {
+          this.file = TorFile.appDir.clone();
+          return;
+        }
+        break;
+      default:
+        if (!TorLauncherUtil.isWindows && this.isIPC) {
+          this.path = "Tor/" + this.ipcFileName;
+          break;
+        }
+        throw new Error("Unknown file type");
+    }
+    if (TorLauncherUtil.isWindows) {
+      this.path = this.path.replace("/", "\\");
+    }
+    this.isRelativePath = true;
+  }
+
+  pathToFile() {
+    // Turn 'path' into an absolute path when needed.
+    if (this.isRelativePath) {
+      const isUserData =
+        this.fileType !== "tor" &&
+        this.fileType !== "pt-startup-dir" &&
+        this.fileType !== "torrc-defaults";
+      if (TorFile.isUserDataOutsideOfAppDir) {
+        let baseDir = isUserData ? TorFile.dataDir : TorFile.appDir;
+        this.file = baseDir.clone();
+      } else {
+        this.file = TorFile.appDir.clone();
+        this.file.append("TorBrowser");
+      }
+      this.file.appendRelativePath(this.path);
+    } else {
+      this.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+      this.file.initWithPath(this.path);
+    }
+  }
+
+  createFile() {
+    if (
+      "tordatadir" == this.fileType ||
+      "toronionauthdir" == this.fileType ||
+      "pt-profiles-dir" == this.fileType
+    ) {
+      this.file.create(this.file.DIRECTORY_TYPE, 0o700);
+    } else {
+      this.file.create(this.file.NORMAL_FILE_TYPE, 0o600);
+    }
+  }
+
+  // If the file exists or an IPC object was requested, normalize the path
+  // and return a file object. The control and SOCKS IPC objects will be
+  // created by tor.
+  normalize() {
+    if (!this.file.exists() && !this.isIPC) {
+      throw new Error(`${this.fileType} file not found: ${this.file.path}`);
+    }
+    try {
+      this.file.normalize();
+    } catch (e) {
+      console.warn("Normalization of the path failed", e);
+    }
+
+    // Ensure that the IPC path length is short enough for use by the
+    // operating system. If not, create and use a unique directory under
+    // /tmp for all IPC objects. The created directory path is stored in
+    // a preference so it can be reused for other IPC objects and so it
+    // can be removed during exit.
+    if (
+      this.isIPC &&
+      this.checkIPCPathLen &&
+      !this.isIPCPathLengthOK(this.file.path, this.extraIPCPathLen)
+    ) {
+      this.file = this.createUniqueIPCDir("/tmp");
+      if (!this.file) {
+        throw new Error("failed to create unique directory under /tmp");
+      }
+
+      Services.prefs.setCharPref(kIPCDirPrefName, this.file.path);
+      this.file.append(this.ipcFileName);
+    }
+  }
+
+  // Return true if aPath is short enough to be used as an IPC object path,
+  // e.g., for a Unix domain socket path. aExtraLen is the "delta" necessary
+  // to accommodate other IPC objects that have longer names; it is used to
+  // account for "control.socket" vs. "socks.socket" (we want to ensure that
+  // all IPC objects are placed in the same parent directory unless the user
+  // has set prefs or env vars to explicitly specify the path for an object).
+  // We enforce a maximum length of 100 because all operating systems allow
+  // at least 100 characters for Unix domain socket paths.
+  isIPCPathLengthOK(aPath, aExtraLen) {
+    const kMaxIPCPathLen = 100;
+    return aPath && aPath.length + aExtraLen <= kMaxIPCPathLen;
+  }
+
+  // Returns an nsIFile or null if a unique directory could not be created.
+  createUniqueIPCDir(aBasePath) {
+    try {
+      const d = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+      d.initWithPath(aBasePath);
+      d.append("Tor");
+      d.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+      return d;
+    } catch (e) {
+      console.error(`createUniqueIPCDir failed for ${aBasePath}: `, e);
+      return null;
+    }
+  }
+
+  static get isUserDataOutsideOfAppDir() {
+    if (this._isUserDataOutsideOfAppDir === undefined) {
+      // Determine if we are using a "side-by-side" data model by checking
+      // whether the user profile is outside of the app directory.
+      try {
+        const profDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+        this._isUserDataOutsideOfAppDir = !this.appDir.contains(profDir);
+      } catch (e) {
+        this._isUserDataOutsideOfAppDir = false;
+      }
+    }
+    return this._isUserDataOutsideOfAppDir;
+  }
+
+  // Returns an nsIFile that points to the application directory.
+  static get appDir() {
+    if (!this._appDir) {
+      let topDir = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+      // On Linux and Windows, we want to return the Browser/ directory.
+      // Because topDir ("CurProcD") points to Browser/browser on those
+      // platforms, we need to go up one level.
+      // On Mac OS, we want to return the TorBrowser.app/ directory.
+      // Because topDir points to Contents/Resources/browser on Mac OS,
+      // we need to go up 3 levels.
+      let tbbBrowserDepth = TorLauncherUtil.isMac ? 3 : 1;
+      while (tbbBrowserDepth > 0) {
+        let didRemove = topDir.leafName != ".";
+        topDir = topDir.parent;
+        if (didRemove) {
+          tbbBrowserDepth--;
+        }
+      }
+      this._appDir = topDir;
+    }
+    return this._appDir;
+  }
+
+  // Returns an nsIFile that points to the TorBrowser-Data/ directory.
+  // This function is only used when isUserDataOutsideOfAppDir === true.
+  // May throw.
+  static get dataDir() {
+    if (!this._dataDir) {
+      const profDir = Services.dirsvc.get("ProfD", Ci.nsIFile);
+      this.mDataDir = profDir.parent.parent;
+    }
+    return this._dataDir;
+  }
+}
+
+const TorLauncherUtil = Object.freeze({
+  get isMac() {
+    return Services.appinfo.OS === "Darwin";
+  },
+
+  get isWindows() {
+    return Services.appinfo.OS === "WINNT";
+  },
+
+  // Returns true if user confirms; false if not.
+  showConfirm(aParentWindow, aMsg, aDefaultButtonLabel, aCancelButtonLabel) {
+    if (!aParentWindow) {
+      aParentWindow = Services.wm.getMostRecentWindow("navigator:browser");
+    }
+
+    const ps = Services.prompt;
+    const title = this.getLocalizedString("error_title");
+    const btnFlags =
+      ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
+      ps.BUTTON_POS_0_DEFAULT +
+      ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
+
+    const notUsed = { value: false };
+    const btnIndex = ps.confirmEx(
+      aParentWindow,
+      title,
+      aMsg,
+      btnFlags,
+      aDefaultButtonLabel,
+      aCancelButtonLabel,
+      null,
+      null,
+      notUsed
+    );
+    return btnIndex === 0;
+  },
+
+  // Localized Strings
+  // TODO: Switch to fluent also these ones.
+
+  // "torlauncher." is prepended to aStringName.
+  getLocalizedString(aStringName) {
+    if (!aStringName) {
+      return aStringName;
+    }
+    try {
+      const key = kPropNamePrefix + aStringName;
+      return this._stringBundle.GetStringFromName(key);
+    } catch (e) {}
+    return aStringName;
+  },
+
+  // "torlauncher." is prepended to aStringName.
+  getFormattedLocalizedString(aStringName, aArray, aLen) {
+    if (!aStringName || !aArray) {
+      return aStringName;
+    }
+    try {
+      const key = kPropNamePrefix + aStringName;
+      return this._stringBundle.formatStringFromName(key, aArray, aLen);
+    } catch (e) {}
+    return aStringName;
+  },
+
+  getLocalizedStringForError(aNSResult) {
+    for (let prop in Cr) {
+      if (Cr[prop] === aNSResult) {
+        const key = "nsresult." + prop;
+        const rv = this.getLocalizedString(key);
+        if (rv !== key) {
+          return rv;
+        }
+        return prop; // As a fallback, return the NS_ERROR... name.
+      }
+    }
+    return undefined;
+  },
+
+  getLocalizedBootstrapStatus(aStatusObj, aKeyword) {
+    if (!aStatusObj || !aKeyword) {
+      return "";
+    }
+
+    let result;
+    let fallbackStr;
+    if (aStatusObj[aKeyword]) {
+      let val = aStatusObj[aKeyword].toLowerCase();
+      let key;
+      if (aKeyword === "TAG") {
+        // The bootstrap status tags in tagMap below are used by Tor
+        // versions prior to 0.4.0.x. We map each one to the tag that will
+        // produce the localized string that is the best fit.
+        const tagMap = {
+          conn_dir: "conn",
+          handshake_dir: "onehop_create",
+          conn_or: "enough_dirinfo",
+          handshake_or: "ap_conn",
+        };
+        if (val in tagMap) {
+          val = tagMap[val];
+        }
+
+        key = "bootstrapStatus." + val;
+        fallbackStr = aStatusObj.SUMMARY;
+      } else if (aKeyword === "REASON") {
+        if (val === "connectreset") {
+          val = "connectrefused";
+        }
+
+        key = "bootstrapWarning." + val;
+        fallbackStr = aStatusObj.WARNING;
+      }
+
+      result = TorLauncherUtil.getLocalizedString(key);
+      if (result === key) {
+        result = undefined;
+      }
+    }
+
+    if (!result) {
+      result = fallbackStr;
+    }
+
+    if (aKeyword === "REASON" && aStatusObj.HOSTADDR) {
+      result += " - " + aStatusObj.HOSTADDR;
+    }
+
+    return result ? result : "";
+  },
+
+  get shouldStartAndOwnTor() {
+    const kPrefStartTor = "extensions.torlauncher.start_tor";
+    try {
+      const kBrowserToolboxPort = "MOZ_BROWSER_TOOLBOX_PORT";
+      const kEnvSkipLaunch = "TOR_SKIP_LAUNCH";
+      const env = Cc["@mozilla.org/process/environment;1"].getService(
+        Ci.nsIEnvironment
+      );
+      if (env.exists(kBrowserToolboxPort)) {
+        return false;
+      }
+      if (env.exists(kEnvSkipLaunch)) {
+        const value = parseInt(env.get(kEnvSkipLaunch));
+        return isNaN(value) || !value;
+      }
+    } catch (e) {}
+    return Services.prefs.getBoolPref(kPrefStartTor, true);
+  },
+
+  get shouldShowNetworkSettings() {
+    try {
+      const kEnvForceShowNetConfig = "TOR_FORCE_NET_CONFIG";
+      const env = Cc["@mozilla.org/process/environment;1"].getService(
+        Ci.nsIEnvironment
+      );
+      if (env.exists(kEnvForceShowNetConfig)) {
+        const value = parseInt(env.get(kEnvForceShowNetConfig));
+        return !isNaN(value) && value;
+      }
+    } catch (e) {}
+    return true;
+  },
+
+  get shouldOnlyConfigureTor() {
+    const kPrefOnlyConfigureTor = "extensions.torlauncher.only_configure_tor";
+    try {
+      const kEnvOnlyConfigureTor = "TOR_CONFIGURE_ONLY";
+      const env = Cc["@mozilla.org/process/environment;1"].getService(
+        Ci.nsIEnvironment
+      );
+      if (env.exists(kEnvOnlyConfigureTor)) {
+        const value = parseInt(env.get(kEnvOnlyConfigureTor));
+        return !isNaN(value) && value;
+      }
+    } catch (e) {}
+    return Services.prefs.getBoolPref(kPrefOnlyConfigureTor, false);
+  },
+
+  // Returns an nsIFile.
+  // If aTorFileType is "control_ipc" or "socks_ipc", aCreate is ignored
+  // and there is no requirement that the IPC object exists.
+  // For all other file types, null is returned if the file does not exist
+  // and it cannot be created (it will be created if aCreate is true).
+  getTorFile(aTorFileType, aCreate) {
+    if (!aTorFileType) {
+      return null;
+    }
+    try {
+      const torFile = new TorFile(aTorFileType, aCreate);
+      return torFile.getFile();
+    } catch (e) {
+      console.error(`getTorFile: cannot get ${aTorFileType}`, e);
+    }
+    return null; // File not found or error (logged above).
+  },
+
+  cleanupTempDirectories() {
+    const dirPath = Services.prefs.getCharPref(kIPCDirPrefName, "");
+    try {
+      Services.prefs.clearUserPref(kIPCDirPrefName);
+    } catch (e) {}
+    try {
+      if (dirPath) {
+        const f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+        f.initWithPath(dirPath);
+        if (f.exists()) {
+          f.remove(false);
+        }
+      }
+    } catch (e) {
+      console.warn("Could not remove the IPC directory", e);
+    }
+  },
+
+  removeMeekAndMoatHelperProfiles() {
+    // FIXME: Is this something we can remove?
+    const removeDirectory = (aParentDir, aName) => {
+      try {
+        const dir = aParentDir.clone();
+        dir.appendRelativePath(aName);
+        if (dir.exists()) {
+          dir.remove(true);
+        }
+      } catch (e) {
+        console.error(`Failed to remove ${aName}:`, e);
+      }
+    };
+
+    const kPrefRemoveHelperProfiles =
+      "extensions.torlauncher.should_remove_meek_helper_profiles";
+    if (Services.prefs.getBoolPref(kPrefRemoveHelperProfiles, false)) {
+      try {
+        // Only attempt removal once.
+        Services.prefs.setBoolPref(kPrefRemoveHelperProfiles, false);
+      } catch (e) {
+        console.warn(`Could not set ${kPrefRemoveHelperProfiles}`, e);
+      }
+
+      if (this.isMac) {
+        let ptProfilesDir = this.getTorFile("pt-profiles-dir", true);
+        if (ptProfilesDir) {
+          removeDirectory(ptProfilesDir, "profile.meek-http-helper");
+          removeDirectory(ptProfilesDir, "profile.moat-http-helper");
+        }
+      }
+    }
+  },
+
+  get _stringBundle() {
+    if (!gStringBundle) {
+      gStringBundle = Services.strings.createBundle(kPropBundleURI);
+    }
+    return gStringBundle;
+  },
+});
diff --git a/toolkit/components/tor-launcher/TorMonitorService.jsm b/toolkit/components/tor-launcher/TorMonitorService.jsm
new file mode 100644
index 000000000000..16d7fc927adc
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorMonitorService.jsm
@@ -0,0 +1,451 @@
+// Copyright (c) 2022, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorMonitorService"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const { TorParsers, TorStatuses } = ChromeUtils.import(
+  "resource://gre/modules/TorParsers.jsm"
+);
+const { TorProcess, TorProcessStatus } = ChromeUtils.import(
+  "resource://gre/modules/TorProcess.jsm"
+);
+
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://gre/modules/TorLauncherUtil.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "controller",
+  "resource://torbutton/modules/tor-control-port.js"
+);
+
+// TODO: Write a helper to create these logs
+XPCOMUtils.defineLazyGetter(this, "logger", () => {
+  const { ConsoleAPI } = ChromeUtils.import(
+    "resource://gre/modules/Console.jsm"
+  );
+  // TODO: Use a preference to set the log level.
+  const consoleOptions = {
+    // maxLogLevel: "warn",
+    maxLogLevel: "all",
+    prefix: "TorMonitorService",
+  };
+  return new ConsoleAPI(consoleOptions);
+});
+
+const Preferences = Object.freeze({
+  PromptAtStartup: "extensions.torlauncher.prompt_at_startup",
+});
+
+const TorTopics = Object.freeze({
+  BootstrapError: "TorBootstrapError",
+  HasWarnOrErr: "TorLogHasWarnOrErr",
+  ProcessDidNotStart: "TorProcessDidNotStart",
+  ProcessExited: "TorProcessExited",
+  ProcessIsReady: "TorProcessIsReady",
+  ProcessRestarted: "TorProcessRestarted",
+});
+
+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 service monitors an existing Tor instance, or starts one, if needed, and
+ * then starts monitoring it.
+ *
+ * This is the service which should be queried to know information about the
+ * status of the bootstrap, the logs, etc...
+ */
+const TorMonitorService = {
+  _connection: null,
+  _eventsToMonitor: Object.freeze(["STATUS_CLIENT", "NOTICE", "WARN", "ERR"]),
+  _torLog: [], // Array of objects with date, type, and msg properties.
+
+  _isBootstrapDone: false,
+  _bootstrapErrorOccurred: false,
+  _lastWarningPhase: null,
+  _lastWarningReason: null,
+
+  _torProcess: null,
+
+  _inited: false,
+
+  // Public methods
+
+  // Starts Tor, if needed, and starts monitoring for events
+  init() {
+    if (this._inited) {
+      return;
+    }
+    this._inited = true;
+    if (this.ownsTorDaemon) {
+      this._controlTor();
+    } else {
+      logger.info(
+        "Not starting the event monitor, as e do not own the Tor daemon."
+      );
+    }
+    logger.debug("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() {
+    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}`);
+    if (!reply) {
+      throw new Error("We received an empty reply");
+    }
+    // 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.lineArray) {
+      this._processBootstrapStatus(reply.lineArray[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;
+  },
+
+  get bootstrapErrorOccurred() {
+    return this._bootstrapErrorOccurred;
+  },
+
+  clearBootstrapError() {
+    this._bootstrapErrorOccurred = false;
+    this._lastWarningPhase = null;
+    this._lastWarningReason = null;
+  },
+
+  // This should be used for debug only
+  setBootstrapError() {
+    this._bootstrapErrorOccurred = true;
+  },
+
+  get isRunning() {
+    return this.ownsTorDaemon
+      ? !!this._torProcess?.isRunning
+      : !!this._connection;
+  },
+
+  // Private methods
+
+  async _startProcess() {
+    this._torProcess = new TorProcess();
+    this._torProcess.onExit = unexpected => {
+      this._shutDownEventMonitor(!unexpected);
+      Services.obs.notifyObservers(null, TorTopics.ProcessExited);
+    };
+    this._torProcess.onRestart = async () => {
+      this._shutDownEventMonitor(false);
+      await this._controlTor();
+      Services.obs.notifyObservers(null, TorTopics.ProcessRestarted);
+    };
+    await this._torProcess.start();
+    if (!this._torProcess.isRunning) {
+      this._torProcess = null;
+      return false;
+    }
+    logger.info("tor started");
+    return true;
+  },
+
+  async _controlTor() {
+    if (!this._torProcess && !(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._status = TorProcessStatus.Running;
+        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.
+        Services.obs.notifyObservers(null, TorTopics.ProcessIsReady);
+      } else if (
+        Date.now() - this._torProcessStartTime >
+        ControlConnTimings.timeoutMS
+      ) {
+        let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
+        TorLauncherUtil.notifyUserOfError(
+          s,
+          null,
+          TorTopics.ProcessDidNotStart
+        );
+        logger.info(s);
+      } else {
+        delayMS *= 2;
+        if (delayMS > ControlConnTimings.maxRetryMS) {
+          delayMS = ControlConnTimings.maxRetryMS;
+        }
+        setTimeout(() => {
+          logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
+          callback();
+        }, delayMS);
+      }
+    };
+    setTimeout(callback, delayMS);
+  },
+
+  async _startEventMonitor() {
+    if (this._connection) {
+      return true;
+    }
+
+    let conn;
+    try {
+      const avoidCache = true;
+      conn = await controller(avoidCache);
+    } catch (e) {
+      logger.error("Cannot open a control port connection", e);
+      return false;
+    }
+
+    // TODO: optionally monitor INFO and DEBUG log messages.
+    let reply = await conn.sendCommand(
+      "SETEVENTS " + this._eventsToMonitor.join(" ")
+    );
+    reply = TorParsers.parseCommandResponse(reply);
+    if (!TorParsers.commandSucceeded(reply)) {
+      logger.error("SETEVENTS failed");
+      conn.close();
+      return false;
+    }
+
+    if (this._torProcess) {
+      this._torProcess.connectionWorked();
+    }
+
+    if (!TorLauncherUtil.shouldOnlyConfigureTor) {
+      this._takeTorOwnership(conn);
+    }
+
+    this._connection = conn;
+    this._waitForEventData();
+    return true;
+  },
+
+  // Try to become the primary controller (TAKEOWNERSHIP).
+  async _takeTorOwnership(conn) {
+    const takeOwnership = "TAKEOWNERSHIP";
+    let reply = await conn.sendCommand(takeOwnership);
+    reply = TorParsers.parseCommandResponse(reply);
+    if (!TorParsers.commandSucceeded(reply)) {
+      logger.warn("Take ownership failed");
+    } else {
+      const resetConf = "RESETCONF __OwningControllerProcess";
+      reply = await conn.sendCommand(resetConf);
+      reply = TorParsers.parseCommandResponse(reply);
+      if (!TorParsers.commandSucceeded(reply)) {
+        logger.warn("Clear owning controller process failed");
+      }
+    }
+  },
+
+  _waitForEventData() {
+    if (!this._connection) {
+      return;
+    }
+    logger.debug("Start watching events:", this._eventsToMonitor);
+    let replyObj = {};
+    for (const torEvent of this._eventsToMonitor) {
+      this._connection.watchEvent(
+        torEvent,
+        null,
+        line => {
+          if (!line) {
+            return;
+          }
+          logger.debug("Event response: ", line);
+          const isComplete = TorParsers.parseReplyLine(line, replyObj);
+          if (isComplete) {
+            this._processEventReply(replyObj);
+            replyObj = {};
+          }
+        },
+        true
+      );
+    }
+  },
+
+  _processEventReply(aReply) {
+    if (aReply._parseError || !aReply.lineArray.length) {
+      return;
+    }
+
+    if (aReply.statusCode !== TorStatuses.EventNotification) {
+      logger.warn("Unexpected event status code:", aReply.statusCode);
+      return;
+    }
+
+    // TODO: do we need to handle multiple lines?
+    const s = aReply.lineArray[0];
+    const idx = s.indexOf(" ");
+    if (idx === -1) {
+      return;
+    }
+    const eventType = s.substring(0, idx);
+    const msg = s.substring(idx + 1).trim();
+
+    if (eventType === "STATUS_CLIENT") {
+      this._processBootstrapStatus(msg, false);
+      return;
+    } else if (!this._eventsToMonitor.includes(eventType)) {
+      logger.debug(`Dropping unlistened event ${eventType}`);
+      return;
+    }
+
+    if (eventType === "WARN" || eventType === "ERR") {
+      // Notify so that Copy Log can be enabled.
+      Services.obs.notifyObservers(null, TorTopics.HasWarnOrErr);
+    }
+
+    const now = 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);
+    }
+    this._torLog.push({ date: now, type: eventType, msg });
+    const logString = `Tor ${eventType}: ${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;
+      this._bootstrapErrorOccurred = false;
+      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) {
+    this._bootstrapErrorOccurred = true;
+    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 msg = TorLauncherUtil.getLocalizedString("tor_bootstrap_failed");
+      TorLauncherUtil.notifyUserOfError(msg, details, TorTopics.BootstrapError);
+    }
+  },
+
+  _shutDownEventMonitor(shouldCallStop = true) {
+    if (this._connection) {
+      if (this.ownsTorDaemon && this._torProcess && shouldCallStop) {
+        this._torProcess.stop();
+        this._torProcess = null;
+      }
+
+      this._connection.close();
+      this._connection = null;
+      this._eventMonitorInProgressReply = null;
+      this._isBootstrapDone = false;
+      this.clearBootstrapError();
+    }
+  },
+};
diff --git a/toolkit/components/tor-launcher/TorParsers.jsm b/toolkit/components/tor-launcher/TorParsers.jsm
new file mode 100644
index 000000000000..1c206fc00e39
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorParsers.jsm
@@ -0,0 +1,275 @@
+// Copyright (c) 2022, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorParsers", "TorStatuses"];
+
+const TorStatuses = Object.freeze({
+  OK: 250,
+  EventNotification: 650,
+});
+
+const TorParsers = Object.freeze({
+  commandSucceeded(aReply) {
+    return aReply?.statusCode === TorStatuses.OK;
+  },
+
+  // parseReply() understands simple GETCONF and GETINFO replies.
+  parseReply(aCmd, aKey, aReply) {
+    if (!aCmd || !aKey || !aReply) {
+      return [];
+    }
+
+    const lcKey = aKey.toLowerCase();
+    const prefix = lcKey + "=";
+    const prefixLen = prefix.length;
+    const tmpArray = [];
+    for (const line of aReply.lineArray) {
+      var lcLine = line.toLowerCase();
+      if (lcLine === lcKey) {
+        tmpArray.push("");
+      } else if (lcLine.indexOf(prefix) !== 0) {
+        console.warn(`Unexpected ${aCmd} response: ${line}`);
+      } else {
+        try {
+          let s = this.unescapeString(line.substring(prefixLen));
+          tmpArray.push(s);
+        } catch (e) {
+          console.warn(
+            `Error while unescaping the response of ${aCmd}: ${line}`,
+            e
+          );
+        }
+      }
+    }
+
+    aReply.lineArray = tmpArray;
+    return aReply;
+  },
+
+  // Returns false if more lines are needed.  The first time, callers
+  // should pass an empty aReplyObj.
+  // Parsing errors are indicated by aReplyObj._parseError = true.
+  parseReplyLine(aLine, aReplyObj) {
+    if (!aLine || !aReplyObj) {
+      return false;
+    }
+
+    if (!("_parseError" in aReplyObj)) {
+      aReplyObj.statusCode = 0;
+      aReplyObj.lineArray = [];
+      aReplyObj._parseError = false;
+    }
+
+    if (aLine.length < 4) {
+      console.error("Unexpected response: ", aLine);
+      aReplyObj._parseError = true;
+      return true;
+    }
+
+    // TODO: handle + separators (data)
+    aReplyObj.statusCode = parseInt(aLine.substring(0, 3), 10);
+    const s = aLine.length < 5 ? "" : aLine.substring(4);
+    // Include all lines except simple "250 OK" ones.
+    if (aReplyObj.statusCode !== TorStatuses.OK || s !== "OK") {
+      aReplyObj.lineArray.push(s);
+    }
+
+    return aLine.charAt(3) === " ";
+  },
+
+  // Split aStr at spaces, accounting for quoted values.
+  // Returns an array of strings.
+  splitReplyLine(aStr) {
+    // Notice: the original function did not check for escaped quotes.
+    return aStr
+      .split('"')
+      .flatMap((token, index) => {
+        const inQuotedStr = index % 2 === 1;
+        return inQuotedStr ? `"${token}"` : token.split(" ");
+      })
+      .filter(s => s);
+  },
+
+  // Helper function for converting a raw controller response into a parsed object.
+  parseCommandResponse(reply) {
+    if (!reply) {
+      return {};
+    }
+    const lines = reply.split("\r\n");
+    const rv = {};
+    for (const line of lines) {
+      if (this.parseReplyLine(line, rv) || rv._parseError) {
+        break;
+      }
+    }
+    return rv;
+  },
+
+  // If successful, returns a JS object with these fields:
+  //   status.TYPE            -- "NOTICE" or "WARN"
+  //   status.PROGRESS        -- integer
+  //   status.TAG             -- string
+  //   status.SUMMARY         -- string
+  //   status.WARNING         -- string (optional)
+  //   status.REASON          -- string (optional)
+  //   status.COUNT           -- integer (optional)
+  //   status.RECOMMENDATION  -- string (optional)
+  //   status.HOSTADDR        -- string (optional)
+  // Returns null upon failure.
+  parseBootstrapStatus(aStatusMsg) {
+    if (!aStatusMsg || !aStatusMsg.length) {
+      return null;
+    }
+
+    let sawBootstrap = false;
+    const statusObj = {};
+    statusObj.TYPE = "NOTICE";
+
+    // The following code assumes that this is a one-line response.
+    for (const tokenAndVal of this.splitReplyLine(aStatusMsg)) {
+      let token, val;
+      const idx = tokenAndVal.indexOf("=");
+      if (idx < 0) {
+        token = tokenAndVal;
+      } else {
+        token = tokenAndVal.substring(0, idx);
+        try {
+          val = TorParsers.unescapeString(tokenAndVal.substring(idx + 1));
+        } catch (e) {
+          console.debug("Could not parse the token value", e);
+        }
+        if (!val) {
+          // skip this token/value pair.
+          continue;
+        }
+      }
+
+      switch (token) {
+        case "BOOTSTRAP":
+          sawBootstrap = true;
+          break;
+        case "WARN":
+        case "NOTICE":
+        case "ERR":
+          statusObj.TYPE = token;
+          break;
+        case "COUNT":
+        case "PROGRESS":
+          statusObj[token] = parseInt(val, 10);
+          break;
+        default:
+          statusObj[token] = val;
+          break;
+      }
+    }
+
+    if (!sawBootstrap) {
+      if (statusObj.TYPE === "NOTICE") {
+        console.info(aStatusMsg);
+      } else {
+        console.warn(aStatusMsg);
+      }
+      return null;
+    }
+
+    return statusObj;
+  },
+
+  // Escape non-ASCII characters for use within the Tor Control protocol.
+  // Based on Vidalia's src/common/stringutil.cpp:string_escape().
+  // Returns the new string.
+  escapeString(aStr) {
+    // Just return if all characters are printable ASCII excluding SP, ", and #
+    const kSafeCharRE = /^[\x21\x24-\x7E]*$/;
+    if (!aStr || kSafeCharRE.test(aStr)) {
+      return aStr;
+    }
+    const escaped = aStr
+      .replace("\\", "\\\\")
+      .replace('"', '\\"')
+      .replace("\n", "\\n")
+      .replace("\r", "\\r")
+      .replace("\t", "\\t")
+      .replace(/[^\x20-\x7e]+/g, text => {
+        const encoder = new TextEncoder();
+        return Array.from(
+          encoder.encode(text),
+          ch => "\\x" + ch.toString(16)
+        ).join("");
+      });
+    return `"${escaped}"`;
+  },
+
+  // Unescape Tor Control string aStr (removing surrounding "" and \ escapes).
+  // Based on Vidalia's src/common/stringutil.cpp:string_unescape().
+  // Returns the unescaped string. Throws upon failure.
+  // Within Torbutton, the file modules/utils.js also contains a copy of
+  // _strUnescape().
+  unescapeString(aStr) {
+    if (
+      !aStr ||
+      aStr.length < 2 ||
+      aStr[0] !== '"' ||
+      aStr[aStr.length - 1] !== '"'
+    ) {
+      return aStr;
+    }
+
+    // Regular expression by Tim Pietzcker
+    // https://stackoverflow.com/a/15569588
+    if (!/^(?:[^"\\]|\\.|"(?:\\.|[^"\\])*")*$/.test(aStr)) {
+      throw new Error('Unescaped " within string');
+    }
+
+    const matchUnicode = /^(\\x[0-9A-Fa-f]{2}|\\[0-7]{3})+/;
+    let rv = "";
+    let lastAdded = 1;
+    let bs;
+    while ((bs = aStr.indexOf("\\", lastAdded)) !== -1) {
+      rv += aStr.substring(lastAdded, bs);
+      // We always increment lastAdded, because we will either add something, or
+      // ignore the backslash.
+      lastAdded = bs + 2;
+      if (lastAdded === aStr.length) {
+        // The string ends with \", which is illegal
+        throw new Error("Missing character after \\");
+      }
+      switch (aStr[bs + 1]) {
+        case "n":
+          rv += "\n";
+          break;
+        case "r":
+          rv += "\r";
+          break;
+        case "t":
+          rv += "\t";
+          break;
+        case '"':
+        case "\\":
+          rv += aStr[bs + 1];
+          break;
+        default:
+          aStr.substring(bs).replace(matchUnicode, sequence => {
+            const bytes = [];
+            for (let i = 0; i < sequence.length; i += 4) {
+              if (sequence[i + 1] === "x") {
+                bytes.push(parseInt(sequence.substring(i + 2, i + 4), 16));
+              } else {
+                bytes.push(parseInt(sequence.substring(i + 1, i + 4), 8));
+              }
+            }
+            lastAdded = bs + sequence.length;
+            const decoder = new TextDecoder();
+            rv += decoder.decode(new Uint8Array(bytes));
+            return "";
+          });
+          // We have already incremented lastAdded, which means we ignore the
+          // backslash, and we will do something at the next one.
+          break;
+      }
+    }
+    rv += aStr.substring(lastAdded, aStr.length - 1);
+    return rv;
+  },
+});
diff --git a/toolkit/components/tor-launcher/TorProcess.jsm b/toolkit/components/tor-launcher/TorProcess.jsm
new file mode 100644
index 000000000000..78bf2504c9b6
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorProcess.jsm
@@ -0,0 +1,551 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProcess", "TorProcessStatus"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorProtocolService",
+  "resource://gre/modules/TorProtocolService.jsm"
+);
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://gre/modules/TorLauncherUtil.jsm"
+);
+
+const { TorParsers } = ChromeUtils.import(
+  "resource://gre/modules/TorParsers.jsm"
+);
+
+const TorProcessStatus = Object.freeze({
+  Unknown: 0,
+  Starting: 1,
+  Running: 2,
+  Exited: 3,
+});
+
+const TorProcessTopics = Object.freeze({
+  ProcessDidNotStart: "TorProcessDidNotStart",
+});
+
+const ProcessTopics = Object.freeze({
+  ProcessFailed: "process-failed",
+  ProcessFinished: "process-finished",
+});
+
+// Logger adapted from CustomizableUI.jsm
+XPCOMUtils.defineLazyGetter(this, "logger", () => {
+  const { ConsoleAPI } = ChromeUtils.import(
+    "resource://gre/modules/Console.jsm"
+  );
+  // TODO: Use a preference to set the log level.
+  const consoleOptions = {
+    maxLogLevel: "info",
+    prefix: "TorProcess",
+  };
+  return new ConsoleAPI(consoleOptions);
+});
+
+class TorProcess {
+  _exeFile = null;
+  _dataDir = null;
+  _args = [];
+  _torProcess = null; // nsIProcess
+  _status = TorProcessStatus.Unknown;
+  _torProcessStartTime = null; // JS Date.now()
+  _didConnectToTorControlPort = false; // Have we ever made a connection?
+
+  onExit = null;
+  onRestart = null;
+
+  get status() {
+    return this._status;
+  }
+
+  get isRunning() {
+    return (
+      this._status === TorProcessStatus.Starting ||
+      this._status === TorProcessStatus.Running
+    );
+  }
+
+  observe(aSubject, aTopic, aParam) {
+    if (
+      ProcessTopics.ProcessFailed === aTopic ||
+      ProcessTopics.ProcessFinished === aTopic
+    ) {
+      this._processExited();
+    }
+  }
+
+  async start() {
+    if (this._torProcess) {
+      return;
+    }
+
+    await this._fixupTorrc();
+
+    this._status = TorProcessStatus.Unknown;
+
+    try {
+      if (!this._makeArgs()) {
+        return;
+      }
+      this._addControlPortArg();
+      this._addSocksPortArg();
+
+      const pid = Services.appinfo.processID;
+      if (pid !== 0) {
+        this._args.push("__OwningControllerProcess");
+        this._args.push("" + pid);
+      }
+
+      if (TorLauncherUtil.shouldShowNetworkSettings) {
+        this._args.push("DisableNetwork");
+        this._args.push("1");
+      }
+
+      // Set an environment variable that points to the Tor data directory.
+      // This is used by meek-client-torbrowser to find the location for
+      // the meek browser profile.
+      const env = Cc["@mozilla.org/process/environment;1"].getService(
+        Ci.nsIEnvironment
+      );
+      env.set("TOR_BROWSER_TOR_DATA_DIR", this._dataDir.path);
+
+      // On Windows, prepend the Tor program directory to PATH. This is needed
+      // so that pluggable transports can find OpenSSL DLLs, etc.
+      // See https://trac.torproject.org/projects/tor/ticket/10845
+      if (TorLauncherUtil.isWindows) {
+        let path = this._exeFile.parent.path;
+        if (env.exists("PATH")) {
+          path += ";" + env.get("PATH");
+        }
+        env.set("PATH", path);
+      }
+
+      this._status = TorProcessStatus.Starting;
+      this._didConnectToTorControlPort = false;
+
+      var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+      p.startHidden = true;
+      p.init(this._exeFile);
+
+      logger.debug(`Starting ${this._exeFile.path}`, this._args);
+
+      // useful for simulating slow tor daemon launch
+      const kPrefTorDaemonLaunchDelay = "extensions.torlauncher.launch_delay";
+      const launchDelay = Services.prefs.getIntPref(
+        kPrefTorDaemonLaunchDelay,
+        0
+      );
+      if (launchDelay > 0) {
+        await new Promise(resolve => setTimeout(() => resolve(), launchDelay));
+      }
+      p.runwAsync(this._args, this._args.length, this, false);
+      // FIXME: This should be okay, unless the observer is called just before
+      // assigning the correct state.
+      // We should check for the control connection status, rather than the
+      // process status.
+      if (this._status === TorProcessStatus.Starting) {
+        this._status = TorProcessStatus.Running;
+      }
+
+      this._torProcess = p;
+      this._torProcessStartTime = Date.now();
+    } catch (e) {
+      this._status = TorProcessStatus.Exited;
+      const s = TorLauncherUtil.getLocalizedString("tor_failed_to_start");
+      TorLauncherUtil.notifyUserOfError(
+        s,
+        null,
+        TorProcessTopics.ProcessDidNotStart
+      );
+      logger.error("startTor error:", e);
+    }
+  }
+
+  stop() {
+    if (this._torProcess) {
+      // We now rely on the TAKEOWNERSHIP feature to shut down tor when we
+      // close the control port connection.
+      //
+      // Previously, we sent a SIGNAL HALT command to the tor control port,
+      // but that caused hangs upon exit in the Firefox 24.x based browser.
+      // Apparently, Firefox does not like to process socket I/O while
+      // quitting if the browser did not finish starting up (e.g., when
+      // someone presses the Quit button on our Network Settings window
+      // during startup).
+      //
+      // However, we set the Subprocess object to null to distinguish the cases
+      // in which we wanted to explicitly kill the tor process, from the cases
+      // in which it exited for any other reason, e.g., a crash.
+      this._torProcess = null;
+    }
+  }
+
+  // The owner of the process can use this function to tell us that they
+  // successfully connected to the control port. This information will be used
+  // only to decide which text to show in the confirmation dialog if tor exits.
+  connectionWorked() {
+    this._didConnectToTorControlPort = true;
+  }
+
+  _processExited() {
+    // When we stop tor intentionally, we also set _torProcess to null.
+    // So, if this._torProcess is not null, it exited for some other reason.
+    const unexpected = !!this._torProcess;
+    if (unexpected) {
+      logger.warn("The tor process exited unexpectedly.");
+    } else {
+      logger.info("The tor process exited.");
+    }
+
+    this._torProcess = null;
+    this._status = TorProcessStatus.Exited;
+
+    let restart = false;
+    if (unexpected) {
+      // TODO: Move this logic somewhere else?
+      let s;
+      if (!this._didConnectToTorControlPort) {
+        // tor might be misconfigured, becauser we could never connect to it
+        const key = "tor_exited_during_startup";
+        s = TorLauncherUtil.getLocalizedString(key);
+      } else {
+        // tor exited suddenly, so configuration should be okay
+        s =
+          TorLauncherUtil.getLocalizedString("tor_exited") +
+          "\n\n" +
+          TorLauncherUtil.getLocalizedString("tor_exited2");
+      }
+      logger.info(s);
+      var defaultBtnLabel = TorLauncherUtil.getLocalizedString("restart_tor");
+      var cancelBtnLabel = "OK";
+      try {
+        const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
+        var sysBundle = Services.strings.createBundle(kSysBundleURI);
+        cancelBtnLabel = sysBundle.GetStringFromName(cancelBtnLabel);
+      } catch (e) {}
+
+      restart = TorLauncherUtil.showConfirm(
+        null,
+        s,
+        defaultBtnLabel,
+        cancelBtnLabel
+      );
+      if (restart) {
+        this.start().then(() => {
+          if (this.onRestart) {
+            this.onRestart();
+          }
+        });
+      }
+    }
+    if (!restart && this.onExit) {
+      this.onExit(unexpected);
+    }
+  }
+
+  _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.
+
+    // 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 = TorProtocolService.torGetPassword(true);
+    let detailsKey;
+    if (!this._exeFile) {
+      detailsKey = "tor_missing";
+    } else if (!torrcFile) {
+      detailsKey = "torrc_missing";
+    } else if (!this._dataDir) {
+      detailsKey = "datadir_missing";
+    } else if (!onionAuthDir) {
+      detailsKey = "onionauthdir_missing";
+    } else if (!hashedPassword) {
+      detailsKey = "password_hash_missing";
+    }
+    if (detailsKey) {
+      const details = TorLauncherUtil.getLocalizedString(detailsKey);
+      const key = "unable_to_start_tor";
+      const err = TorLauncherUtil.getFormattedLocalizedString(
+        key,
+        [details],
+        1
+      );
+      TorLauncherUtil.notifyUserOfError(
+        err,
+        null,
+        TorProcessTopics.ProcessDidNotStart
+      );
+      return false;
+    }
+
+    const torrcDefaultsFile = TorLauncherUtil.getTorFile(
+      "torrc-defaults",
+      false
+    );
+    // The geoip and geoip6 files are in the same directory as torrc-defaults.
+    const geoipFile = torrcDefaultsFile.clone();
+    geoipFile.leafName = "geoip";
+    const geoip6File = torrcDefaultsFile.clone();
+    geoip6File.leafName = "geoip6";
+
+    this._args = [];
+    if (torrcDefaultsFile) {
+      this._args.push("--defaults-torrc");
+      this._args.push(torrcDefaultsFile.path);
+    }
+    this._args.push("-f");
+    this._args.push(torrcFile.path);
+    this._args.push("DataDirectory");
+    this._args.push(this._dataDir.path);
+    this._args.push("ClientOnionAuthDir");
+    this._args.push(onionAuthDir.path);
+    this._args.push("GeoIPFile");
+    this._args.push(geoipFile.path);
+    this._args.push("GeoIPv6File");
+    this._args.push(geoip6File.path);
+    this._args.push("HashedControlPassword");
+    this._args.push(hashedPassword);
+
+    return true;
+  }
+
+  _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.
+    let controlPortArg;
+    const controlIPCFile = TorProtocolService.torGetControlIPCFile();
+    const controlPort = TorProtocolService.torGetControlPort();
+    if (controlIPCFile) {
+      controlPortArg = this._ipcPortArg(controlIPCFile);
+    } else if (controlPort) {
+      controlPortArg = "" + controlPort;
+    }
+    if (controlPortArg) {
+      this._args.push("+__ControlPort");
+      this._args.push(controlPortArg);
+    }
+  }
+
+  _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 = 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 (socksPortArg) {
+        let socksPortFlags = Services.prefs.getCharPref(
+          "extensions.torlauncher.socks_port_flags",
+          "IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"
+        );
+        if (socksPortFlags) {
+          socksPortArg += " " + socksPortFlags;
+        }
+        this._args.push("+__SocksPort");
+        this._args.push(socksPortArg);
+      }
+    }
+  }
+
+  // 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);
+  }
+
+  async _fixupTorrc() {
+    // If we have not already done so, remove any ControlPort and SocksPort
+    // lines from the user's torrc file that may conflict with the arguments
+    // we plan to pass when starting tor.
+    // See bugs 20761 and 22283.
+    const kTorrcFixupVersion = 2;
+    const kTorrcFixupPref = "extensions.torlauncher.torrc_fixup_version";
+    if (Services.prefs.getIntPref(kTorrcFixupPref, 0) > kTorrcFixupVersion) {
+      return true;
+    }
+
+    let torrcFile = TorLauncherUtil.getTorFile("torrc", true);
+    if (!torrcFile) {
+      // No torrc file; nothing to fixup.
+      return true;
+    }
+    torrcFile = torrcFile.path;
+
+    let torrcStr;
+    try {
+      torrcStr = await IOUtils.readUTF8(torrcFile);
+    } catch (e) {
+      logger.error(`Could not read ${torrcFile}:`, e);
+      return false;
+    }
+    if (!torrcStr.length) {
+      return true;
+    }
+
+    const controlIPCFile = TorProtocolService.torGetControlIPCFile();
+    const controlPort = TorProtocolService.torGetControlPort();
+    const socksPortInfo = TorProtocolService.torGetSOCKSPortInfo();
+
+    const valueIsUnixDomainSocket = aValue => {
+      // Handle several cases:
+      //  "unix:/path options"
+      //  unix:"/path" options
+      //  unix:/path options
+      if (aValue.startsWith('"')) {
+        aValue = TorParsers.unescapeString(aValue);
+      }
+      return aValue.startsWith("unix:");
+    };
+    const valueContainsPort = (aValue, aPort) => {
+      // Check for a match, ignoring "127.0.0.1" and "localhost" prefixes.
+      let val = TorParsers.unescapeString(aValue);
+      const pieces = val.split(":");
+      if (
+        pieces.length >= 2 &&
+        (pieces[0] === "127.0.0.1" || pieces[0].toLowerCase() === "localhost")
+      ) {
+        val = pieces[1];
+      }
+      return aPort === parseInt(val);
+    };
+
+    let removedLinesCount = 0;
+    const revisedLines = [];
+    const lines = this._joinContinuedTorrcLines(torrcStr);
+    lines.forEach(aLine => {
+      let removeLine = false;
+      // Look for "+ControlPort value" or "ControlPort value", skipping leading
+      // whitespace and ignoring case.
+      let matchResult = aLine.match(/\s*\+*controlport\s+(.*)/i);
+      if (matchResult) {
+        removeLine = valueIsUnixDomainSocket(matchResult[1]);
+        if (!removeLine && !controlIPCFile) {
+          removeLine = valueContainsPort(matchResult[1], controlPort);
+        }
+      } else if (socksPortInfo) {
+        // Look for "+SocksPort value" or "SocksPort value", skipping leading
+        // whitespace and ignoring case.
+        matchResult = aLine.match(/\s*\+*socksport\s+(.*)/i);
+        if (matchResult) {
+          removeLine = valueIsUnixDomainSocket(matchResult[1]);
+          if (!removeLine && !socksPortInfo.ipcFile) {
+            removeLine = valueContainsPort(matchResult[1], socksPortInfo.port);
+          }
+        }
+      }
+
+      if (removeLine) {
+        ++removedLinesCount;
+        logger.info(`fixupTorrc: removing ${aLine}`);
+      } else {
+        revisedLines.push(aLine);
+      }
+    });
+
+    if (removedLinesCount > 0) {
+      const data = new TextEncoder().encode(revisedLines.join("\n"));
+      try {
+        await IOUtils.write(torrcFile, data, {
+          tmpPath: torrcFile + ".tmp",
+        });
+      } catch (e) {
+        logger.error(`Failed to overwrite file ${torrcFile}:`, e);
+        return false;
+      }
+      logger.info(
+        `fixupTorrc: removed ${removedLinesCount} configuration options`
+      );
+    }
+
+    Services.prefs.setIntPref(kTorrcFixupPref, kTorrcFixupVersion);
+    return true;
+  }
+
+  // Split aTorrcStr into lines, joining continued lines.
+  _joinContinuedTorrcLines(aTorrcStr) {
+    const lines = [];
+    const rawLines = aTorrcStr.split("\n");
+    let isContinuedLine = false;
+    let tmpLine;
+    rawLines.forEach(aLine => {
+      let len = aLine.length;
+
+      // Strip trailing CR if present.
+      if (len > 0 && aLine.substr(len - 1) === "\r") {
+        --len;
+        aLine = aLine.substr(0, len);
+      }
+
+      // Check for a continued line. This is indicated by a trailing \ or, if
+      // we are already within a continued line sequence, a trailing comment.
+      if (len > 0 && aLine.substr(len - 1) === "\\") {
+        --len;
+        aLine = aLine.substr(0, len);
+
+        // If this is the start of a continued line and it only contains a
+        // keyword (i.e., no spaces are present), append a space so that
+        // the keyword will be recognized (as it is by tor) after we join
+        // the pieces of the continued line into one line.
+        if (!isContinuedLine && !aLine.includes(" ")) {
+          aLine += " ";
+        }
+
+        isContinuedLine = true;
+      } else if (isContinuedLine) {
+        if (!len) {
+          isContinuedLine = false;
+        } else {
+          // Check for a comment. According to tor's doc/torrc_format.txt,
+          // comments do not terminate a sequence of continued lines.
+          let idx = aLine.indexOf("#");
+          if (idx < 0) {
+            isContinuedLine = false; // Not a comment; end continued line.
+          } else {
+            // Remove trailing comment from continued line. The continued
+            // line sequence continues.
+            aLine = aLine.substr(0, idx);
+          }
+        }
+      }
+
+      if (isContinuedLine) {
+        if (tmpLine) {
+          tmpLine += aLine;
+        } else {
+          tmpLine = aLine;
+        }
+      } else if (tmpLine) {
+        lines.push(tmpLine + aLine);
+        tmpLine = undefined;
+      } else {
+        lines.push(aLine);
+      }
+    });
+
+    return lines;
+  }
+}
diff --git a/toolkit/components/tor-launcher/TorProtocolService.jsm b/toolkit/components/tor-launcher/TorProtocolService.jsm
new file mode 100644
index 000000000000..3ccffd6883f5
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorProtocolService.jsm
@@ -0,0 +1,749 @@
+// Copyright (c) 2021, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProtocolService"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+ChromeUtils.defineModuleGetter(
+  this,
+  "FileUtils",
+  "resource://gre/modules/FileUtils.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+Cu.importGlobalProperties(["crypto"]);
+
+const { TorParsers } = ChromeUtils.import(
+  "resource://gre/modules/TorParsers.jsm"
+);
+const { TorLauncherUtil } = ChromeUtils.import(
+  "resource://gre/modules/TorLauncherUtil.jsm"
+);
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorMonitorService",
+  "resource://gre/modules/TorMonitorService.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "configureControlPortModule",
+  "resource://torbutton/modules/tor-control-port.js"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "controller",
+  "resource://torbutton/modules/tor-control-port.js"
+);
+
+const TorTopics = Object.freeze({
+  ProcessExited: "TorProcessExited",
+  ProcessRestarted: "TorProcessRestarted",
+});
+
+// Logger adapted from CustomizableUI.jsm
+XPCOMUtils.defineLazyGetter(this, "logger", () => {
+  const { ConsoleAPI } = ChromeUtils.import(
+    "resource://gre/modules/Console.jsm"
+  );
+  // TODO: Use a preference to set the log level.
+  const consoleOptions = {
+    // maxLogLevel: "warn",
+    maxLogLevel: "all",
+    prefix: "TorProtocolService",
+  };
+  return new ConsoleAPI(consoleOptions);
+});
+
+// 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.
+const TorProtocolService = {
+  _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(),
+
+  _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: [],
+
+  // Public methods
+
+  async init() {
+    if (this._inited) {
+      return;
+    }
+    this._inited = true;
+
+    Services.obs.addObserver(this, TorTopics.ProcessExited);
+    Services.obs.addObserver(this, TorTopics.ProcessRestarted);
+
+    await this._setSockets();
+
+    logger.debug("TorProtocolService initialized");
+  },
+
+  uninit() {
+    Services.obs.removeObserver(this, TorTopics.ProcessExited);
+    Services.obs.removeObserver(this, TorTopics.ProcessRestarted);
+    this._closeConnection();
+  },
+
+  observe(subject, topic, data) {
+    if (topic === TorTopics.ProcessExited) {
+      this._closeConnection();
+    } else if (topic === TorTopics.ProcessRestarted) {
+      this._reconnect();
+    }
+  },
+
+  // takes a Map containing tor settings
+  // throws on error
+  async writeSettings(aSettingsObj) {
+    // 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);
+
+      if (!this._settingsCache.has(setting)) {
+        // no cached setting, so write
+        return true;
+      }
+
+      const cachedValue = this._settingsCache.get(setting);
+      if (value === cachedValue) {
+        return false;
+      } else if (Array.isArray(value) && Array.isArray(cachedValue)) {
+        // compare arrays member-wise
+        if (value.length !== cachedValue.length) {
+          return true;
+        }
+        for (let i = 0; i < value.length; i++) {
+          if (value[i] !== cachedValue[i]) {
+            return true;
+          }
+        }
+        return false;
+      }
+      // some other different values
+      return true;
+    });
+
+    // only write if new setting to save
+    if (newSettings.length) {
+      const settingsObject = Object.fromEntries(newSettings);
+      await this.setConfWithReply(settingsObject);
+
+      // save settings to cache after successfully writing to Tor
+      for (const [setting, value] of newSettings) {
+        this._settingsCache.set(setting, value);
+      }
+    }
+  },
+
+  async readStringArraySetting(aSetting) {
+    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";
+    const settings = {};
+    settings[kTorConfKeyDisableNetwork] = false;
+    await this.setConfWithReply(settings);
+    await this.sendCommand("SAVECONF");
+    TorMonitorService.clearBootstrapError();
+    TorMonitorService.retrieveBootstrapStatus();
+  },
+
+  async stopBootstrap() {
+    // Tell tor to disable use of the network; this should stop the bootstrap
+    // process.
+    try {
+      const settings = { DisableNetwork: true };
+      await this.setConfWithReply(settings);
+    } catch (e) {
+      logger.error("Error stopping bootstrap", e);
+    }
+    // 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.
+    TorMonitorService.retrieveBootstrapStatus();
+  },
+
+  // TODO: transform the following 4 functions in getters. At the moment they
+  // are also used in torbutton.
+
+  // Returns Tor password string or null if an error occurs.
+  torGetPassword(aPleaseHash) {
+    const pw = this._controlPassword;
+    return aPleaseHash ? this._hashPassword(pw) : pw;
+  },
+
+  torGetControlIPCFile() {
+    return this._controlIPCFile?.clone();
+  },
+
+  torGetControlPort() {
+    return this._controlPort;
+  },
+
+  torGetSOCKSPortInfo() {
+    return this._SOCKSPortInfo;
+  },
+
+  // Public, but called only internally
+
+  // Executes a command on the control port.
+  // Return a reply object or null if a fatal error occurs.
+  async sendCommand(cmd, args) {
+    let conn, reply;
+    const maxAttempts = 2;
+    for (let attempt = 0; !reply && attempt < maxAttempts; attempt++) {
+      try {
+        conn = await this._getConnection();
+        try {
+          if (conn) {
+            reply = await conn.sendCommand(cmd + (args ? " " + args : ""));
+            if (reply) {
+              // Return for reuse.
+              this._returnConnection();
+            } else {
+              // Connection is bad.
+              this._closeConnection();
+            }
+          }
+        } catch (e) {
+          logger.error("Cannot send a command", e);
+          this._closeConnection();
+        }
+      } catch (e) {
+        logger.error("Cannot get a connection to the control port", e);
+      }
+    }
+
+    // We failed to acquire the controller after multiple attempts.
+    // Try again after some time.
+    if (!conn) {
+      logger.info(
+        "sendCommand: Acquiring control connection failed",
+        cmd,
+        args
+      );
+      return new Promise(resolve =>
+        setTimeout(() => {
+          resolve(this.sendCommand(cmd, args));
+        }, 250)
+      );
+    }
+
+    if (!reply) {
+      throw new Error(`${cmd} sent an empty response`);
+    }
+
+    // TODO: Move the parsing of the reply to the controller, because anyone
+    // calling sendCommand on it actually wants a parsed reply.
+
+    reply = TorParsers.parseCommandResponse(reply);
+    if (!TorParsers.commandSucceeded(reply)) {
+      if (reply?.lineArray) {
+        throw new Error(reply.lineArray.join("\n"));
+      }
+      throw new Error(`${cmd} failed with code ${reply.statusCode}`);
+    }
+
+    return reply;
+  },
+
+  // Perform a SETCONF command.
+  // aSettingsObj should be a JavaScript object with keys (property values)
+  // that correspond to tor config. keys. The value associated with each
+  // key should be a simple string, a string array, or a Boolean value.
+  // If an associated value is undefined or null, a key with no value is
+  // passed in the SETCONF command.
+  // Throws in case of error, or returns a reply object.
+  async setConfWithReply(settings) {
+    if (!settings) {
+      throw new Error("Empty settings object");
+    }
+    const args = Object.entries(settings)
+      .map(([key, val]) => {
+        if (val === undefined || val === null) {
+          return key;
+        }
+        const valType = typeof val;
+        let rv = `${key}=`;
+        if (valType === "boolean") {
+          rv += val ? "1" : "0";
+        } else if (Array.isArray(val)) {
+          rv += val.map(TorParsers.escapeString).join("\n");
+        } else if (valType === "string") {
+          rv += TorParsers.escapeString(val);
+        } else {
+          logger.error(`Got unsupported type for ${key}`, val);
+          throw new Error(`Unsupported type ${valType} (key ${key})`);
+        }
+        return rv;
+      })
+      .filter(arg => arg);
+    if (!args.length) {
+      throw new Error("No settings to set");
+    }
+
+    await this.sendCommand("SETCONF", args.join(" "));
+  },
+
+  // Public, never called?
+
+  async readBoolSetting(aSetting) {
+    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);
+    return value;
+  },
+
+  // Private
+
+  async _setSockets() {
+    try {
+      const isWindows = TorLauncherUtil.isWindows;
+      const env = Cc["@mozilla.org/process/environment;1"].getService(
+        Ci.nsIEnvironment
+      );
+      // Determine how Tor Launcher will connect to the Tor control port.
+      // Environment variables get top priority followed by preferences.
+      if (!isWindows && env.exists("TOR_CONTROL_IPC_PATH")) {
+        const ipcPath = env.get("TOR_CONTROL_IPC_PATH");
+        this._controlIPCFile = new FileUtils.File(ipcPath);
+      } else {
+        // Check for TCP host and port environment variables.
+        if (env.exists("TOR_CONTROL_HOST")) {
+          this._controlHost = env.get("TOR_CONTROL_HOST");
+        }
+        if (env.exists("TOR_CONTROL_PORT")) {
+          this._controlPort = parseInt(env.get("TOR_CONTROL_PORT"), 10);
+        }
+
+        const useIPC =
+          !isWindows &&
+          Services.prefs.getBoolPref(
+            "extensions.torlauncher.control_port_use_ipc",
+            false
+          );
+        if (!this._controlHost && !this._controlPort && useIPC) {
+          this._controlIPCFile = TorLauncherUtil.getTorFile(
+            "control_ipc",
+            false
+          );
+        } else {
+          if (!this._controlHost) {
+            this._controlHost = Services.prefs.getCharPref(
+              "extensions.torlauncher.control_host",
+              "127.0.0.1"
+            );
+          }
+          if (!this._controlPort) {
+            this._controlPort = Services.prefs.getIntPref(
+              "extensions.torlauncher.control_port",
+              9151
+            );
+          }
+        }
+      }
+
+      // Populate _controlPassword so it is available when starting tor.
+      if (env.exists("TOR_CONTROL_PASSWD")) {
+        this._controlPassword = env.get("TOR_CONTROL_PASSWD");
+      } else if (env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
+        // TODO: test this code path (TOR_CONTROL_COOKIE_AUTH_FILE).
+        const cookiePath = env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
+        if (cookiePath) {
+          this._controlPassword = await this._readAuthenticationCookie(
+            cookiePath
+          );
+        }
+      }
+      if (!this._controlPassword) {
+        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 && env.exists("TOR_SOCKS_IPC_PATH")) {
+        let ipcPath = env.get("TOR_SOCKS_IPC_PATH");
+        this._SOCKSPortInfo.ipcFile = new FileUtils.File(ipcPath);
+        useIPC = true;
+      } else {
+        // Check for TCP host and port environment variables.
+        if (env.exists("TOR_SOCKS_HOST")) {
+          this._SOCKSPortInfo.host = env.get("TOR_SOCKS_HOST");
+          useIPC = false;
+        }
+        if (env.exists("TOR_SOCKS_PORT")) {
+          this._SOCKSPortInfo.port = parseInt(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}`);
+      }
+
+      // Set the global control port info parameters.
+      // These values may be overwritten by torbutton when it initializes, but
+      // torbutton's values *should* be identical.
+      configureControlPortModule(
+        this._controlIPCFile,
+        this._controlHost,
+        this._controlPort,
+        this._controlPassword
+      );
+    } catch (e) {
+      logger.error("Failed to get environment variables", e);
+    }
+  },
+
+  _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);
+    switch (typeof aValue) {
+      case "boolean":
+      case "string":
+        return;
+      case "object":
+        if (aValue === null) {
+          return;
+        } else if (Array.isArray(aValue)) {
+          for (const element of aValue) {
+            if (typeof element !== "string") {
+              throw new Error(
+                `Setting '${aSetting}' array contains value of invalid type '${typeof element}'`
+              );
+            }
+          }
+          return;
+        }
+      // fall through
+      default:
+        throw new Error(
+          `Invalid object type received for setting '${aSetting}'`
+        );
+    }
+  },
+
+  // Perform a GETCONF command.
+  async _readSetting(aSetting) {
+    this._assertValidSettingKey(aSetting);
+
+    const cmd = "GETCONF";
+    let reply = await this.sendCommand(cmd, aSetting);
+    reply = TorParsers.parseReply(cmd, aSetting, reply);
+    if (TorParsers.commandSucceeded(reply)) {
+      return reply.lineArray;
+    }
+    throw new Error(reply.lineArray.join("\n"));
+  },
+
+  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);
+    switch (value) {
+      case "0":
+        return false;
+      case "1":
+        return true;
+      default:
+        throw new Error(`Expected boolean (1 or 0) but received '${value}'`);
+    }
+  },
+
+  // Opens an authenticated connection, sets it to this._controlConnection, and
+  // return it.
+  async _getConnection() {
+    if (!this._controlConnection) {
+      const avoidCache = true;
+      this._controlConnection = await controller(avoidCache);
+    }
+    if (this._controlConnection.inUse) {
+      await new Promise((resolve, reject) =>
+        this._connectionQueue.push({ resolve, reject })
+      );
+    } else {
+      this._controlConnection.inUse = true;
+    }
+    return this._controlConnection;
+  },
+
+  _returnConnection() {
+    if (this._connectionQueue.length) {
+      this._connectionQueue.shift().resolve();
+    } else {
+      this._controlConnection.inUse = false;
+    }
+  },
+
+  // If aConn is omitted, the cached connection is closed.
+  _closeConnection() {
+    if (this._controlConnection) {
+      logger.info("Closing the control connection");
+      this._controlConnection.close();
+      this._controlConnection = null;
+    }
+    for (const promise of this._connectionQueue) {
+      promise.reject("Connection closed");
+    }
+    this._connectionQueue = [];
+  },
+
+  async _reconnect() {
+    this._closeConnection();
+    const conn = await this._getConnection();
+    logger.debug("Reconnected to the control port.");
+    this._returnConnection(conn);
+  },
+
+  async _readAuthenticationCookie(aPath) {
+    const bytes = await IOUtils.read(aPath);
+    return Array.from(bytes, b => this._toHex(b, 2)).join("");
+  },
+
+  // Returns a random 16 character password, hex-encoded.
+  _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);
+      if (val < 0) {
+        logger.error("_cryptoRandInt() failed");
+        return null;
+      }
+      pwd += this._toHex(kMinCharCode + val, 2);
+    }
+
+    return pwd;
+  },
+
+  // Based on Vidalia's TorSettings::hashPassword().
+  _hashPassword(aHexPassword) {
+    if (!aHexPassword) {
+      return null;
+    }
+
+    // Generate a random, 8 byte salt value.
+    const salt = Array.from(crypto.getRandomValues(new Uint8Array(8)));
+
+    // Convert hex-encoded password to an array of bytes.
+    const password = new Array();
+    for (let i = 0; i < aHexPassword.length; i += 2) {
+      password.push(parseInt(aHexPassword.substring(i, i + 2), 16));
+    }
+
+    // Run through the S2K algorithm and convert to a string.
+    const kCodedCount = 96;
+    const hashVal = this._cryptoSecretToKey(password, salt, kCodedCount);
+    if (!hashVal) {
+      logger.error("_cryptoSecretToKey() failed");
+      return null;
+    }
+
+    const arrayToHex = aArray =>
+      aArray.map(item => this._toHex(item, 2)).join("");
+    let rv = "16:";
+    rv += arrayToHex(salt);
+    rv += this._toHex(kCodedCount, 2);
+    rv += arrayToHex(hashVal);
+    return rv;
+  },
+
+  // Returns -1 upon failure.
+  _cryptoRandInt(aMax) {
+    // Based on tor's crypto_rand_int().
+    const maxUInt = 0xffffffff;
+    if (aMax <= 0 || aMax > maxUInt) {
+      return -1;
+    }
+
+    const cutoff = maxUInt - (maxUInt % aMax);
+    let val = cutoff;
+    while (val >= cutoff) {
+      const uint32 = new Uint32Array(1);
+      crypto.getRandomValues(uint32);
+      val = uint32[0];
+    }
+    return val % aMax;
+  },
+
+  // _cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key().
+  // It generates and returns a hash of aPassword by following the iterated
+  // and salted S2K algorithm (see RFC 2440 section 3.6.1.3).
+  // Returns an array of bytes.
+  _cryptoSecretToKey(aPassword, aSalt, aCodedCount) {
+    if (!aPassword || !aSalt) {
+      return null;
+    }
+
+    const inputArray = aSalt.concat(aPassword);
+
+    // Subtle crypto only has the final digest, and does not allow incremental
+    // updates. Also, it is async, so we should hash and keep the hash in a
+    // variable if we wanted to switch to getters.
+    // So, keeping this implementation should be okay for now.
+    const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+      Ci.nsICryptoHash
+    );
+    hasher.init(hasher.SHA1);
+    const kEXPBIAS = 6;
+    let count = (16 + (aCodedCount & 15)) << ((aCodedCount >> 4) + kEXPBIAS);
+    while (count > 0) {
+      if (count > inputArray.length) {
+        hasher.update(inputArray, inputArray.length);
+        count -= inputArray.length;
+      } else {
+        const finalArray = inputArray.slice(0, count);
+        hasher.update(finalArray, finalArray.length);
+        count = 0;
+      }
+    }
+    return hasher
+      .finish(false)
+      .split("")
+      .map(b => b.charCodeAt(0));
+  },
+
+  _toHex(aValue, aMinLen) {
+    return aValue.toString(16).padStart(aMinLen, "0");
+  },
+};
diff --git a/toolkit/components/tor-launcher/TorStartupService.jsm b/toolkit/components/tor-launcher/TorStartupService.jsm
new file mode 100644
index 000000000000..9c27b34b2f6f
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorStartupService.jsm
@@ -0,0 +1,76 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorStartupService"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+// We will use the modules only when the profile is loaded, so prefer lazy
+// loading
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorLauncherUtil",
+  "resource://gre/modules/TorLauncherUtil.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorMonitorService",
+  "resource://gre/modules/TorMonitorService.jsm"
+);
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorProtocolService",
+  "resource://gre/modules/TorProtocolService.jsm"
+);
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+  ProfileAfterChange: "profile-after-change",
+  QuitApplicationGranted: "quit-application-granted",
+});
+
+let gInited = false;
+
+// This class is registered as an observer, and will be instanced automatically
+// by Firefox.
+// When it observes profile-after-change, it initializes whatever is needed to
+// launch Tor.
+class TorStartupService {
+  _defaultPreferencesAreLoaded = false;
+
+  observe(aSubject, aTopic, aData) {
+    if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
+      this._init();
+    } else if (aTopic === BrowserTopics.QuitApplicationGranted) {
+      this._uninit();
+    }
+  }
+
+  async _init() {
+    Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
+
+    // Starts TorProtocolService first, because it configures the controller
+    // factory, too.
+    await TorProtocolService.init();
+    TorMonitorService.init();
+
+    try {
+      TorLauncherUtil.removeMeekAndMoatHelperProfiles();
+    } catch (e) {
+      console.warn("Failed to remove meek and moat profiles", e);
+    }
+
+    gInited = true;
+  }
+
+  _uninit() {
+    Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
+
+    // Close any helper connection first...
+    TorProtocolService.uninit();
+    // ... and only then closes the event monitor connection, which will cause
+    // Tor to stop.
+    TorMonitorService.uninit();
+
+    TorLauncherUtil.cleanupTempDirectories();
+  }
+}
diff --git a/toolkit/components/tor-launcher/components.conf b/toolkit/components/tor-launcher/components.conf
new file mode 100644
index 000000000000..4e62a7a1e24f
--- /dev/null
+++ b/toolkit/components/tor-launcher/components.conf
@@ -0,0 +1,10 @@
+Classes = [
+    {
+        "cid": "{df46c65d-be2b-4d16-b280-69733329eecf}",
+        "contract_ids": [
+            "@torproject.org/tor-startup-service;1"
+        ],
+        "jsm": "resource://gre/modules/TorStartupService.jsm",
+        "constructor": "TorStartupService",
+    },
+]
diff --git a/toolkit/components/tor-launcher/moz.build b/toolkit/components/tor-launcher/moz.build
new file mode 100644
index 000000000000..2b3d00077168
--- /dev/null
+++ b/toolkit/components/tor-launcher/moz.build
@@ -0,0 +1,17 @@
+EXTRA_JS_MODULES += [
+    "TorBootstrapRequest.jsm",
+    "TorLauncherUtil.jsm",
+    "TorMonitorService.jsm",
+    "TorParsers.jsm",
+    "TorProcess.jsm",
+    "TorProtocolService.jsm",
+    "TorStartupService.jsm",
+]
+
+XPCOM_MANIFESTS += [
+    "components.conf",
+]
+
+EXTRA_COMPONENTS += [
+    "tor-launcher.manifest",
+]
diff --git a/toolkit/components/tor-launcher/tor-launcher.manifest b/toolkit/components/tor-launcher/tor-launcher.manifest
new file mode 100644
index 000000000000..649f3419e825
--- /dev/null
+++ b/toolkit/components/tor-launcher/tor-launcher.manifest
@@ -0,0 +1 @@
+category profile-after-change TorStartupService @torproject.org/tor-startup-service;1

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the tbb-commits mailing list