[tbb-commits] [tor-browser] 52/74: Bug 40933: Add tor-launcher functionality
gitolite role
git at cupani.torproject.org
Thu Oct 27 21:21:57 UTC 2022
This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch tor-browser-102.4.0esr-12.0-2
in repository tor-browser.
commit c40c5a6f141bd6ad44341fe69d2381899cbd45e0
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 | 129 ++++
.../components/tor-launcher/TorLauncherUtil.jsm | 603 +++++++++++++++++
.../components/tor-launcher/TorMonitorService.jsm | 507 ++++++++++++++
toolkit/components/tor-launcher/TorParsers.jsm | 275 ++++++++
toolkit/components/tor-launcher/TorProcess.jsm | 535 +++++++++++++++
.../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, 2904 insertions(+)
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
index bc0803aef688..cb7891d5b43e 100644
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -234,6 +234,7 @@
@RESPATH@/browser/chrome/browser.manifest
@RESPATH@/chrome/pdfjs.manifest
@RESPATH@/chrome/pdfjs/*
+ at RESPATH@/components/tor-launcher.manifest
@RESPATH@/chrome/toolkit at JAREXT@
@RESPATH@/chrome/toolkit.manifest
#ifdef MOZ_GTK
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..e999d5c3f62c
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorBootstrapRequest.jsm
@@ -0,0 +1,129 @@
+"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;
+ }
+
+ 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: {
+ console.info("TorBootstrapRequest: observerd TorBootstrapError", obj);
+ this._stop(obj?.message, obj?.details);
+ 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 () => {
+ this._timeoutID = null;
+ // TODO: Translate, if really used
+ await this._stop(
+ "Tor Bootstrap process timed out",
+ `Bootstrap attempt abandoned after waiting ${this.timeout} ms`
+ );
+ }, this.timeout);
+ }
+
+ // wait for bootstrapping to begin and maybe handle error
+ TorProtocolService.connect().catch(err => {
+ this._stop(err.message, "");
+ });
+ }).finally(() => {
+ // and remove ourselves once bootstrap is resolved
+ Services.obs.removeObserver(this, TorTopics.BootstrapStatus);
+ Services.obs.removeObserver(this, TorTopics.BootstrapError);
+ this._bootstrapPromise = null;
+ });
+
+ return this._bootstrapPromise;
+ }
+
+ async cancel() {
+ await this._stop();
+ }
+
+ // Internal implementation. Do not use directly, but call cancel, instead.
+ async _stop(message, details) {
+ // first stop our bootstrap timeout before handling the error
+ if (this._timeoutID !== null) {
+ clearTimeout(this._timeoutID);
+ this._timeoutID = null;
+ }
+
+ // stopBootstrap never throws
+ await TorProtocolService.stopBootstrap();
+
+ if (this.onbootstraperror && message) {
+ this.onbootstraperror(message, details);
+ }
+
+ 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..798040fa3abf
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorLauncherUtil.jsm
@@ -0,0 +1,603 @@
+// 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.replaceAll("/", "\\");
+ }
+ this.isRelativePath = true;
+ }
+
+ pathToFile() {
+ if (TorLauncherUtil.isWindows) {
+ this.path = this.path.replaceAll("/", "\\");
+ }
+ // 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._dataDir = 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..27572c30370f
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorMonitorService.jsm
@@ -0,0 +1,507 @@
+// Copyright (c) 2022, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorMonitorService"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { clearTimeout, 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 } = 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",
+ 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.
+ _startTimeout: null,
+
+ _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 we 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() {
+ if (this._torProcess) {
+ this._torProcess.forget();
+ this._torProcess.onExit = null;
+ this._torProcess.onRestart = null;
+ this._torProcess = null;
+ }
+ this._shutDownEventMonitor();
+ },
+
+ async retrieveBootstrapStatus() {
+ if (!this._connection) {
+ throw new Error("Event monitor connection not available");
+ }
+
+ // TODO: Unify with TorProtocolService.sendCommand and put everything in the
+ // reviewed torbutton replacement.
+ const cmd = "GETINFO";
+ const key = "status/bootstrap-phase";
+ let reply = await this._connection.sendCommand(`${cmd} ${key}`);
+ 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() {
+ // TorProcess should be instanced once, then always reused and restarted
+ // only through the prompt it exposes when the controlled process dies.
+ if (!this._torProcess) {
+ this._torProcess = new TorProcess();
+ this._torProcess.onExit = () => {
+ this._shutDownEventMonitor();
+ Services.obs.notifyObservers(null, TorTopics.ProcessExited);
+ };
+ this._torProcess.onRestart = async () => {
+ this._shutDownEventMonitor();
+ await this._controlTor();
+ Services.obs.notifyObservers(null, TorTopics.ProcessRestarted);
+ };
+ }
+
+ // Already running, but we did not start it
+ if (this._torProcess.isRunning) {
+ return false;
+ }
+
+ try {
+ await this._torProcess.start();
+ if (this._torProcess.isRunning) {
+ logger.info("tor started");
+ }
+ } catch (e) {
+ // TorProcess already logs the error.
+ this._bootstrapErrorOccurred = true;
+ this._lastWarningPhase = "startup";
+ this._lastWarningReason = e.toString();
+ }
+ return this._torProcess.isRunning;
+ },
+
+ async _controlTor() {
+ if (!this._torProcess?.isRunning && !(await this._startProcess())) {
+ logger.error("Tor not running, not starting to monitor it.");
+ return;
+ }
+
+ let delayMS = ControlConnTimings.initialDelayMS;
+ const callback = async () => {
+ if (await this._startEventMonitor()) {
+ this.retrieveBootstrapStatus().catch(e => {
+ logger.warn("Could not get the initial bootstrap status", e);
+ });
+
+ // FIXME: TorProcess is misleading here. We should use a topic related
+ // to having a control port connection, instead.
+ Services.obs.notifyObservers(null, TorTopics.ProcessIsReady);
+
+ // We reset this here hoping that _shutDownEventMonitor can interrupt
+ // the current monitor, either by calling clearTimeout and preventing it
+ // from starting, or by closing the control port connection.
+ if (this._startTimeout === null) {
+ logger.warn("Someone else reset _startTimeout!");
+ }
+ this._startTimeout = null;
+ } else if (
+ Date.now() - this._torProcessStartTime >
+ ControlConnTimings.timeoutMS
+ ) {
+ let s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
+ this._bootstrapErrorOccurred = true;
+ this._lastWarningPhase = "startup";
+ this._lastWarningReason = s;
+ logger.info(s);
+ if (this._startTimeout === null) {
+ logger.warn("Someone else reset _startTimeout!");
+ }
+ this._startTimeout = null;
+ } else {
+ delayMS *= 2;
+ if (delayMS > ControlConnTimings.maxRetryMS) {
+ delayMS = ControlConnTimings.maxRetryMS;
+ }
+ this._startTimeout = setTimeout(() => {
+ logger.debug(`Control port not ready, waiting ${delayMS / 1000}s.`);
+ callback();
+ }, delayMS);
+ }
+ };
+ // Check again, in the unfortunate case in which the execution was alrady
+ // queued, but was waiting network code.
+ if (this._startTimeout === null) {
+ this._startTimeout = setTimeout(callback, delayMS);
+ } else {
+ logger.error("Possible race? Refusing to start the timeout again");
+ }
+ },
+
+ async _startEventMonitor() {
+ if (this._connection) {
+ return true;
+ }
+
+ let conn;
+ try {
+ const avoidCache = true;
+ conn = await controller(avoidCache);
+ } catch (e) {
+ logger.error("Cannot open a control port connection", e);
+ if (conn) {
+ try {
+ conn.close();
+ } catch (e) {
+ logger.error(
+ "Also, the connection is not null but cannot be closed",
+ e
+ );
+ }
+ }
+ return false;
+ }
+
+ // TODO: optionally monitor INFO and DEBUG log messages.
+ 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;
+ }
+
+ // FIXME: At the moment it is not possible to start the event monitor
+ // when we do start the tor process. So, does it make sense to keep this
+ // control?
+ if (this._torProcess) {
+ this._torProcess.connectionWorked();
+ }
+
+ if (!TorLauncherUtil.shouldOnlyConfigureTor) {
+ try {
+ await this._takeTorOwnership(conn);
+ } catch (e) {
+ logger.warn("Could not take ownership of the Tor daemon", e);
+ }
+ }
+
+ 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 message = TorLauncherUtil.getLocalizedString(
+ "tor_bootstrap_failed"
+ );
+ Services.obs.notifyObservers(
+ { message, details },
+ TorTopics.BootstrapError
+ );
+ }
+ },
+
+ _shutDownEventMonitor() {
+ this._connection?.close();
+ this._connection = null;
+ if (this._startTimeout !== null) {
+ clearTimeout(this._startTimeout);
+ this._startTimeout = 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..a8ad7b73c95e
--- /dev/null
+++ b/toolkit/components/tor-launcher/TorProcess.jsm
@@ -0,0 +1,535 @@
+"use strict";
+
+var EXPORTED_SYMBOLS = ["TorProcess"];
+
+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 { Subprocess } = ChromeUtils.import(
+ "resource://gre/modules/Subprocess.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,
+});
+
+// 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 = [];
+ _subprocess = null;
+ _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
+ );
+ }
+
+ async start() {
+ if (this._subprocess) {
+ return;
+ }
+
+ await this._fixupTorrc();
+
+ this._status = TorProcessStatus.Unknown;
+
+ try {
+ this._makeArgs();
+ 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 environment = {
+ 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;
+ const env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+ );
+ if (env.exists("PATH")) {
+ path += ";" + env.get("PATH");
+ }
+ environment.PATH = path;
+ }
+
+ this._status = TorProcessStatus.Starting;
+ this._didConnectToTorControlPort = false;
+
+ // 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));
+ }
+
+ logger.debug(`Starting ${this._exeFile.path}`, this._args);
+ const options = {
+ command: this._exeFile.path,
+ arguments: this._args,
+ environment,
+ environmentAppend: true,
+ stderr: "pipe",
+ };
+ this._subprocess = await Subprocess.call(options);
+ this._watchProcess();
+ this._status = TorProcessStatus.Running;
+ this._torProcessStartTime = Date.now();
+ } catch (e) {
+ this._status = TorProcessStatus.Exited;
+ this._subprocess = null;
+ logger.error("startTor error:", e);
+ throw e;
+ }
+ }
+
+ // Forget about a process.
+ //
+ // Instead of killing the tor process, we 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).
+ //
+ // Still, before closing the owning connection, this class should forget about
+ // the process, so that future notifications will be ignored.
+ forget() {
+ this._subprocess = null;
+ this._status = TorProcessStatus.Exited;
+ }
+
+ // 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;
+ }
+
+ async _watchProcess() {
+ const watched = this._subprocess;
+ if (!watched) {
+ return;
+ }
+ try {
+ const { exitCode } = await watched.wait();
+
+ if (watched !== this._subprocess) {
+ logger.debug(`A Tor process exited with code ${exitCode}.`);
+ } else if (exitCode) {
+ logger.warn(`The watched Tor process exited with code ${exitCode}.`);
+ } else {
+ logger.info("The Tor process exited.");
+ }
+ } catch (e) {
+ logger.error("Failed to watch the tor process", e);
+ }
+
+ if (watched === this._subprocess) {
+ this._processExitedUnexpectedly();
+ }
+ }
+
+ _processExitedUnexpectedly() {
+ this._subprocess = null;
+ this._status = TorProcessStatus.Exited;
+
+ // 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);
+ const defaultBtnLabel = TorLauncherUtil.getLocalizedString("restart_tor");
+ let cancelBtnLabel = "OK";
+ try {
+ const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
+ const sysBundle = Services.strings.createBundle(kSysBundleURI);
+ cancelBtnLabel = sysBundle.GetStringFromName(cancelBtnLabel);
+ } catch (e) {
+ logger.warn("Could not localize the cancel button", e);
+ }
+
+ const restart = TorLauncherUtil.showConfirm(
+ null,
+ s,
+ defaultBtnLabel,
+ cancelBtnLabel
+ );
+ if (restart) {
+ this.start().then(() => {
+ if (this.onRestart) {
+ this.onRestart();
+ }
+ });
+ } else if (this.onExit) {
+ this.onExit();
+ }
+ }
+
+ _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
+ );
+ throw new Error(err);
+ }
+
+ 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);
+ }
+
+ _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