This is an automated email from the git hooks/post-receive script.
richard pushed a change to branch tor-browser-102.3.0esr-12.0-2 in repository tor-browser.
from 93c9881b2a05 fixup! Base Browser's .mozconfigs. new 6fc29f91fbe8 fixup! TB3: Tor Browser's official .mozconfigs. new d294beea175d Revert "Bug 28044: Integrate Tor Launcher into tor-browser" new ec870ce68851 Bug 40933: Add tor-launcher functionality new 1a64d858c5ab fixup! Bug 40933: Add tor-launcher functionality new 07710cca4425 fixup! Bug 40562: Added Tor-related preferences to 000-tor-browser.js new 2df6696ded66 fixup! Bug 40597: Implement TorSettings module new c3726ddc7c05 fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser new eeb101368c6e fixup! Bug 4234: Use the Firefox Update Process for Tor Browser. new 91abcc7a6a7f fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection new 0a0945855cb7 fixup! Bug 10760: Integrate TorButton to TorBrowser core
The 10 revisions listed above as "new" are entirely new to this repository and will be described in separate emails. The revisions listed as "add" were already present in the repository and have only been added to this reference.
Summary of changes: browser/app/profile/000-tor-browser.js | 69 ++ browser/components/sessionstore/SessionStore.jsm | 5 - .../torpreferences/content/connectionPane.js | 11 +- .../torpreferences/content/torLogDialog.jsm | 6 +- browser/config/mozconfigs/tor-browser-android | 3 - browser/extensions/moz.build | 3 - browser/installer/Makefile.in | 4 - browser/installer/package-manifest.in | 6 +- browser/modules/BridgeDB.jsm | 2 +- browser/modules/Moat.jsm | 4 +- browser/modules/TorConnect.jsm | 73 +- browser/modules/TorProcessService.jsm | 12 - browser/modules/TorProtocolService.jsm | 510 -------------- browser/modules/TorSettings.jsm | 25 +- browser/modules/moz.build | 2 - moz.configure | 18 - mozconfig-linux-x86_64-dev | 1 - toolkit/components/moz.build | 1 + .../processsingleton/MainProcessSingleton.jsm | 4 - .../tor-launcher/TorBootstrapRequest.jsm | 131 ++++ .../components/tor-launcher/TorLauncherUtil.jsm | 600 +++++++++++++++++ .../components/tor-launcher/TorMonitorService.jsm | 449 ++++++++++++ toolkit/components/tor-launcher/TorParsers.jsm | 275 ++++++++ toolkit/components/tor-launcher/TorProcess.jsm | 552 +++++++++++++++ .../components/tor-launcher/TorProtocolService.jsm | 749 +++++++++++++++++++++ .../components/tor-launcher/TorStartupService.jsm | 90 +++ toolkit/components/tor-launcher/components.conf | 10 + toolkit/components/tor-launcher/moz.build | 17 + .../components/tor-launcher/tor-launcher.manifest | 1 + toolkit/mozapps/update/UpdateService.jsm | 8 +- toolkit/torproject/torbutton | 2 +- 31 files changed, 3003 insertions(+), 640 deletions(-) delete mode 100644 browser/modules/TorProcessService.jsm delete mode 100644 browser/modules/TorProtocolService.jsm create mode 100644 toolkit/components/tor-launcher/TorBootstrapRequest.jsm create mode 100644 toolkit/components/tor-launcher/TorLauncherUtil.jsm create mode 100644 toolkit/components/tor-launcher/TorMonitorService.jsm create mode 100644 toolkit/components/tor-launcher/TorParsers.jsm create mode 100644 toolkit/components/tor-launcher/TorProcess.jsm create mode 100644 toolkit/components/tor-launcher/TorProtocolService.jsm create mode 100644 toolkit/components/tor-launcher/TorStartupService.jsm create mode 100644 toolkit/components/tor-launcher/components.conf create mode 100644 toolkit/components/tor-launcher/moz.build create mode 100644 toolkit/components/tor-launcher/tor-launcher.manifest
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 6fc29f91fbe8576a3b935c44f3aa4674d4980a57 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:34:25 2022 +0200
fixup! TB3: Tor Browser's official .mozconfigs.
Bug 40933: Move tor-launcher to tor-browser.git --- browser/config/mozconfigs/tor-browser-android | 3 --- browser/installer/Makefile.in | 4 ---- moz.configure | 18 ------------------ mozconfig-linux-x86_64-dev | 1 - 4 files changed, 26 deletions(-)
diff --git a/browser/config/mozconfigs/tor-browser-android b/browser/config/mozconfigs/tor-browser-android index 1aef19a587a3..484f6303c6b4 100644 --- a/browser/config/mozconfigs/tor-browser-android +++ b/browser/config/mozconfigs/tor-browser-android @@ -2,9 +2,6 @@
mk_add_options MOZ_APP_DISPLAYNAME="Tor Browser"
-# We do not use Tor Launcher on Android: -ac_add_options --disable-tor-launcher - ac_add_options --disable-tor-browser-update ac_add_options --disable-verify-mar
diff --git a/browser/installer/Makefile.in b/browser/installer/Makefile.in index f5411547c0a7..e1e0a15e86a0 100644 --- a/browser/installer/Makefile.in +++ b/browser/installer/Makefile.in @@ -82,10 +82,6 @@ endif endif endif
-ifdef TOR_BROWSER_DISABLE_TOR_LAUNCHER -DEFINES += -DTOR_BROWSER_DISABLE_TOR_LAUNCHER -endif - ifdef TOR_BROWSER_UPDATE DEFINES += -DTOR_BROWSER_UPDATE endif diff --git a/moz.configure b/moz.configure index d7073eba80b7..7ec6d25a8ac1 100755 --- a/moz.configure +++ b/moz.configure @@ -1107,24 +1107,6 @@ add_old_configure_assignment( "TOR_BROWSER_DATA_OUTSIDE_APP_DIR", tor_browser_data_outside_app_dir)
-option( - "--disable-tor-launcher", - help="Do not include Tor Launcher" -) - - -@depends("--disable-tor-launcher") -def tor_browser_disable_launcher(value): - if not value: - return True - - -set_config("TOR_BROWSER_DISABLE_TOR_LAUNCHER", tor_browser_disable_launcher) -set_define("TOR_BROWSER_DISABLE_TOR_LAUNCHER", tor_browser_disable_launcher) -add_old_configure_assignment( - "TOR_BROWSER_DISABLE_TOR_LAUNCHER", tor_browser_disable_launcher) - - # Please do not add configure checks from here on.
# Fallthrough to autoconf-based configure diff --git a/mozconfig-linux-x86_64-dev b/mozconfig-linux-x86_64-dev index 06d16d40fcf1..491fc507362f 100644 --- a/mozconfig-linux-x86_64-dev +++ b/mozconfig-linux-x86_64-dev @@ -9,7 +9,6 @@ ac_add_options --enable-default-toolkit=cairo-gtk3 ac_add_options --disable-strip ac_add_options --disable-install-strip
-ac_add_options --enable-tor-launcher ac_add_options --disable-tor-browser-update ac_add_options --with-tor-browser-version=dev-build ac_add_options --without-wasm-sandboxed-libraries
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 d294beea175deafae4cef7011f0ea8c2d825ef17 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:23:40 2022 +0200
Revert "Bug 28044: Integrate Tor Launcher into tor-browser"
This reverts commit 149a3ffb1fce6fbaf1e479bfa6d2463f1d7ce88f. --- browser/extensions/moz.build | 3 --- browser/installer/package-manifest.in | 5 ----- 2 files changed, 8 deletions(-)
diff --git a/browser/extensions/moz.build b/browser/extensions/moz.build index df3a43a07fd7..39bbc2937271 100644 --- a/browser/extensions/moz.build +++ b/browser/extensions/moz.build @@ -5,6 +5,3 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += ["onboarding"] - -if not CONFIG["TOR_BROWSER_DISABLE_TOR_LAUNCHER"]: - DIRS += ["tor-launcher"] diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index c84928e45c15..ff87c7e39db8 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -236,11 +236,6 @@ @RESPATH@/browser/chrome/browser.manifest @RESPATH@/chrome/pdfjs.manifest @RESPATH@/chrome/pdfjs/* -#ifndef TOR_BROWSER_DISABLE_TOR_LAUNCHER -@RESPATH@/browser/chrome/torlauncher.manifest -@RESPATH@/browser/chrome/torlauncher/* -@RESPATH@/browser/@PREF_DIR@/torlauncher-prefs.js -#endif @RESPATH@/chrome/torbutton.manifest @RESPATH@/chrome/torbutton/* @RESPATH@/chrome/toolkit@JAREXT@
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@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/* +@RESPATH@/components/tor-launcher.manifest @RESPATH@/chrome/torbutton.manifest @RESPATH@/chrome/torbutton/* @RESPATH@/chrome/toolkit@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
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 1a64d858c5ab472eec6fc48a43b9ff54942971a5 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:26:50 2022 +0200
fixup! Bug 40933: Add tor-launcher functionality
Bug 40853: Use Subprocess.jsm to launch tor. --- .../components/tor-launcher/TorMonitorService.jsm | 16 +- toolkit/components/tor-launcher/TorProcess.jsm | 205 +++++++++++---------- 2 files changed, 110 insertions(+), 111 deletions(-)
diff --git a/toolkit/components/tor-launcher/TorMonitorService.jsm b/toolkit/components/tor-launcher/TorMonitorService.jsm index 16d7fc927adc..201ac6275c56 100644 --- a/toolkit/components/tor-launcher/TorMonitorService.jsm +++ b/toolkit/components/tor-launcher/TorMonitorService.jsm @@ -103,6 +103,10 @@ const TorMonitorService = { // 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 = null; + } this._shutDownEventMonitor(); },
@@ -179,12 +183,11 @@ const TorMonitorService = {
async _startProcess() { this._torProcess = new TorProcess(); - this._torProcess.onExit = unexpected => { - this._shutDownEventMonitor(!unexpected); + this._torProcess.onExit = () => { Services.obs.notifyObservers(null, TorTopics.ProcessExited); }; this._torProcess.onRestart = async () => { - this._shutDownEventMonitor(false); + this._shutDownEventMonitor(); await this._controlTor(); Services.obs.notifyObservers(null, TorTopics.ProcessRestarted); }; @@ -434,13 +437,8 @@ const TorMonitorService = { } },
- _shutDownEventMonitor(shouldCallStop = true) { + _shutDownEventMonitor() { if (this._connection) { - if (this.ownsTorDaemon && this._torProcess && shouldCallStop) { - this._torProcess.stop(); - this._torProcess = null; - } - this._connection.close(); this._connection = null; this._eventMonitorInProgressReply = null; diff --git a/toolkit/components/tor-launcher/TorProcess.jsm b/toolkit/components/tor-launcher/TorProcess.jsm index 78bf2504c9b6..a23fd324efff 100644 --- a/toolkit/components/tor-launcher/TorProcess.jsm +++ b/toolkit/components/tor-launcher/TorProcess.jsm @@ -8,6 +8,10 @@ const { XPCOMUtils } = ChromeUtils.import( "resource://gre/modules/XPCOMUtils.jsm" );
+const { Subprocess } = ChromeUtils.import( + "resource://gre/modules/Subprocess.jsm" +); + ChromeUtils.defineModuleGetter( this, "TorProtocolService", @@ -32,11 +36,6 @@ 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( @@ -73,15 +72,6 @@ class TorProcess { ); }
- observe(aSubject, aTopic, aParam) { - if ( - ProcessTopics.ProcessFailed === aTopic || - ProcessTopics.ProcessFinished === aTopic - ) { - this._processExited(); - } - } - async start() { if (this._torProcess) { return; @@ -112,31 +102,27 @@ class TorProcess { // 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); + 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"); } - env.set("PATH", path); + environment.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( @@ -146,46 +132,48 @@ class TorProcess { 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; + logger.debug(`Starting ${this._exeFile.path}`, this._args); + const options = { + command: this._exeFile.path, + arguments: this._args, + environment, + environmentAppend: true, + stderr: "pipe", + }; + this._torProcess = await Subprocess.call(options); + this._watchProcess(); + this._status = TorProcessStatus.Running; this._torProcessStartTime = Date.now(); } catch (e) { this._status = TorProcessStatus.Exited; - const s = TorLauncherUtil.getLocalizedString("tor_failed_to_start"); - TorLauncherUtil.notifyUserOfError( - s, + this._torProcess = null; + logger.error("startTor error:", e); + Services.obs.notifyObservers( null, - TorProcessTopics.ProcessDidNotStart + TorProcessTopics.ProcessDidNotStart, + null ); - 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; - } + // 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._torProcess = null; + this._status = TorProcessStatus.Exited; }
// The owner of the process can use this function to tell us that they @@ -195,58 +183,71 @@ class TorProcess { 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."); + async _watchProcess() { + const watched = this._torProcess; + if (!watched) { + return; } + try { + const { exitCode } = await watched.wait();
- 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); + if (watched !== this._torProcess) { + logger.debug(`A Tor process exited with code ${exitCode}.`); + } else if (exitCode) { + logger.warn(`The watched Tor process exited with code ${exitCode}.`); } else { - // tor exited suddenly, so configuration should be okay - s = - TorLauncherUtil.getLocalizedString("tor_exited") + - "\n\n" + - TorLauncherUtil.getLocalizedString("tor_exited2"); + logger.info("The Tor process exited."); } - 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) {} + } catch (e) { + logger.error("Failed to watch the tor process", e); + }
- restart = TorLauncherUtil.showConfirm( - null, - s, - defaultBtnLabel, - cancelBtnLabel - ); - if (restart) { - this.start().then(() => { - if (this.onRestart) { - this.onRestart(); - } - }); - } + if (watched === this._torProcess) { + this._processExitedUnexpectedly(); } - if (!restart && this.onExit) { + } + + _processExitedUnexpectedly() { + this._torProcess = 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(unexpected); } }
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 07710cca442514727d8e545c1b6a7c2eee4905f7 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:14:14 2022 +0200
fixup! Bug 40562: Added Tor-related preferences to 000-tor-browser.js
Bug 40933: Added tor-launcher defaults --- browser/app/profile/000-tor-browser.js | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+)
diff --git a/browser/app/profile/000-tor-browser.js b/browser/app/profile/000-tor-browser.js index 9a269f5a9632..1a992d9681ad 100644 --- a/browser/app/profile/000-tor-browser.js +++ b/browser/app/profile/000-tor-browser.js @@ -94,3 +94,72 @@ pref("browser.startup.homepage", "about:tor");
// This pref specifies an ad-hoc "version" for various pref update hacks we need to do pref("extensions.torbutton.pref_fixup_version", 0); + +// Formerly tor-launcher defaults +// When presenting the setup wizard, first prompt for locale. +pref("intl.locale.matchOS", true); +pref("extensions.torlauncher.prompt_for_locale", true); + +pref("extensions.torlauncher.start_tor", true); +pref("extensions.torlauncher.prompt_at_startup", true); +pref("extensions.torlauncher.quickstart", false); + +// This pref controls whether Tor Launcher will try to remove the old +// meek and moat http helper browser profiles. This only has an effect +// on macOS; for Windows and Linux profile removal is handled by the +// updater (since on those platforms the profiles are embedded within +// the browser install directory). +pref("extensions.torlauncher.should_remove_meek_helper_profiles", true); + +pref("extensions.torlauncher.loglevel", 4); // 1=verbose, 2=debug, 3=info, 4=note, 5=warn +pref("extensions.torlauncher.logmethod", 1); // 0=stdout, 1=errorconsole, 2=debuglog +pref("extensions.torlauncher.max_tor_log_entries", 1000); + +// By default, Tor Launcher configures a TCP listener for the Tor +// control port, as defined by control_host and control_port. +// Set control_port_use_ipc to true to use an IPC object (e.g., a Unix +// domain socket) instead. You may also modify control_ipc_path to +// override the default IPC object location. If a relative path is used, +// it is handled like torrc_path (see below). +pref("extensions.torlauncher.control_host", "127.0.0.1"); +pref("extensions.torlauncher.control_port", 9151); +pref("extensions.torlauncher.control_port_use_ipc", false); +pref("extensions.torlauncher.control_ipc_path", ""); + +// By default, Tor Launcher configures a TCP listener for the Tor +// SOCKS port. The host is taken from the network.proxy.socks pref and +// the port is taken from the network.proxy.socks_port pref. +// Set socks_port_use_ipc to true to use an IPC object (e.g., a Unix +// domain socket) instead. You may also modify socks_ipc_path to +// override the default IPC object location. If a relative path is used, +// it is handled like torrc_path (see below). +// Modify socks_port_flags to use a different set of SocksPort flags (but be +// careful). +pref("extensions.torlauncher.socks_port_use_ipc", false); +pref("extensions.torlauncher.socks_ipc_path", ""); +pref("extensions.torlauncher.socks_port_flags", "ExtendedErrors IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"); + +// The tor_path is relative to the application directory. On Linux and +// Windows this is the Browser/ directory that contains the firefox +// executables, and on Mac OS it is the TorBrowser.app directory. +pref("extensions.torlauncher.tor_path", ""); + +// The torrc_path and tordatadir_path are relative to the data directory, +// which is TorBrowser-Data/ if it exists as a sibling of the application +// directory. If TorBrowser-Data/ does not exist, these paths are relative +// to the TorBrowser/ directory within the application directory. +pref("extensions.torlauncher.torrc_path", ""); +pref("extensions.torlauncher.tordatadir_path", ""); + +// BridgeDB-related preferences (used for Moat). +pref("extensions.torlauncher.bridgedb_front", "cdn.sstatic.net"); +pref("extensions.torlauncher.bridgedb_reflector", "https://moat.torproject.org.global.prod.fastly.net/"); +pref("extensions.torlauncher.moat_service", "https://bridges.torproject.org/moat"); +pref("extensions.torlauncher.bridgedb_bridge_type", "obfs4"); + +// Recommended default bridge type (can be set per localized bundle). +// pref("extensions.torlauncher.default_bridge_recommended_type", "obfs3"); + +// Default bridges. +// pref("extensions.torlauncher.default_bridge.TYPE.1", "TYPE x.x.x.x:yy"); +// pref("extensions.torlauncher.default_bridge.TYPE.2", "TYPE x.x.x.x:yy");
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 2df6696ded66b62ca03109d3db7ab8f9b56e1cff Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:17:46 2022 +0200
fixup! Bug 40597: Implement TorSettings module
Bug 40933: Move tor-launcher to tor-browser.git --- browser/components/sessionstore/SessionStore.jsm | 5 - browser/modules/BridgeDB.jsm | 2 +- browser/modules/Moat.jsm | 4 +- browser/modules/TorConnect.jsm | 73 ++- browser/modules/TorProcessService.jsm | 12 - browser/modules/TorProtocolService.jsm | 510 --------------------- browser/modules/TorSettings.jsm | 25 +- browser/modules/moz.build | 2 - .../processsingleton/MainProcessSingleton.jsm | 2 - .../components/tor-launcher/TorStartupService.jsm | 14 + 10 files changed, 57 insertions(+), 592 deletions(-)
diff --git a/browser/components/sessionstore/SessionStore.jsm b/browser/components/sessionstore/SessionStore.jsm index 5f9f6aab2e54..58c5a4fed6ca 100644 --- a/browser/components/sessionstore/SessionStore.jsm +++ b/browser/components/sessionstore/SessionStore.jsm @@ -234,11 +234,6 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/sessionstore/SessionHistory.jsm" );
-// FIXME: Is this really necessary? -const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" -); - const { OnionAuthUtil } = ChromeUtils.import( "chrome://browser/content/onionservices/authUtil.jsm" ); diff --git a/browser/modules/BridgeDB.jsm b/browser/modules/BridgeDB.jsm index 50665710ebf4..3110dfbbf20f 100644 --- a/browser/modules/BridgeDB.jsm +++ b/browser/modules/BridgeDB.jsm @@ -46,7 +46,7 @@ var BridgeDB = { this._image = "data:image/jpeg;base64," + encodeURIComponent(response.image); } catch (err) { - console.log(`error : ${err}`); + console.error("Could not request a captcha image", err); } return this._image; }, diff --git a/browser/modules/Moat.jsm b/browser/modules/Moat.jsm index 90a6ae4e521c..28f177e27b3b 100644 --- a/browser/modules/Moat.jsm +++ b/browser/modules/Moat.jsm @@ -9,11 +9,11 @@ const { Subprocess } = ChromeUtils.import( );
const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" + "resource://gre/modules/TorLauncherUtil.jsm" );
const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" + "resource://gre/modules/TorProtocolService.jsm" );
const { TorSettings, TorBridgeSource } = ChromeUtils.import( diff --git a/browser/modules/TorConnect.jsm b/browser/modules/TorConnect.jsm index cc0eeb2b1eba..34a3a9ee7d33 100644 --- a/browser/modules/TorConnect.jsm +++ b/browser/modules/TorConnect.jsm @@ -17,14 +17,14 @@ const { BrowserWindowTracker } = ChromeUtils.import( "resource:///modules/BrowserWindowTracker.jsm" );
-const { - TorProtocolService, - TorTopics, - TorBootstrapRequest, -} = ChromeUtils.import("resource:///modules/TorProtocolService.jsm"); - +const { TorMonitorService } = ChromeUtils.import( + "resource://gre/modules/TorMonitorService.jsm" +); +const { TorBootstrapRequest } = ChromeUtils.import( + "resource://gre/modules/TorBootstrapRequest.jsm" +); const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" + "resource://gre/modules/TorLauncherUtil.jsm" );
const { @@ -37,9 +37,9 @@ const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
const { MoatRPC } = ChromeUtils.import("resource:///modules/Moat.jsm");
-/* Browser observer topis */ -const BrowserTopics = Object.freeze({ - ProfileAfterChange: "profile-after-change", +const TorTopics = Object.freeze({ + LogHasWarnOrErr: "TorLogHasWarnOrErr", + ProcessExited: "TorProcessExited", });
/* Relevant prefs used by tor-launcher */ @@ -445,7 +445,7 @@ const TorConnect = (() => { "Error: Censorship simulation", true ); - TorProtocolService._torBootstrapDebugSetError(); + TorMonitorService.setBootstrapError(); return; }
@@ -567,7 +567,7 @@ const TorConnect = (() => { ); return; } - TorProtocolService._torBootstrapDebugSetError(); + TorMonitorService.setBootstrapError(); } }
@@ -817,42 +817,31 @@ const TorConnect = (() => { ); },
- // init should be called on app-startup in MainProcessingSingleton.jsm + // init should be called by TorStartupService init() { console.log("TorConnect: init()"); - - // delay remaining init until after profile-after-change - Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); - this._callback(TorConnectState.Initial).begin(); + + if (!TorMonitorService.ownsTorDaemon) { + // Disabled + this._changeState(TorConnectState.Disabled); + } else { + let observeTopic = addTopic => { + Services.obs.addObserver(this, addTopic); + console.log(`TorConnect: Observing topic '${addTopic}'`); + }; + + // register the Tor topics we always care about + observeTopic(TorTopics.ProcessExited); + observeTopic(TorTopics.LogHasWarnOrErr); + observeTopic(TorSettingsTopics.Ready); + } },
async observe(subject, topic, data) { console.log(`TorConnect: Observed ${topic}`);
switch (topic) { - /* Determine which state to move to from Initial */ - case BrowserTopics.ProfileAfterChange: { - if ( - TorLauncherUtil.useLegacyLauncher || - !TorProtocolService.ownsTorDaemon - ) { - // Disabled - this._changeState(TorConnectState.Disabled); - } else { - let observeTopic = addTopic => { - Services.obs.addObserver(this, addTopic); - console.log(`TorConnect: Observing topic '${addTopic}'`); - }; - - // register the Tor topics we always care about - observeTopic(TorTopics.ProcessExited); - observeTopic(TorTopics.LogHasWarnOrErr); - observeTopic(TorSettingsTopics.Ready); - } - Services.obs.removeObserver(this, topic); - break; - } /* We need to wait until TorSettings have been loaded and applied before we can Quickstart */ case TorSettingsTopics.Ready: { if (this.shouldQuickStart) { @@ -881,11 +870,9 @@ const TorConnect = (() => { get shouldShowTorConnect() { // TorBrowser must control the daemon return ( - TorProtocolService.ownsTorDaemon && - // and we're not using the legacy launcher - !TorLauncherUtil.useLegacyLauncher && + TorMonitorService.ownsTorDaemon && // if we have succesfully bootstraped, then no need to show TorConnect - this.state != TorConnectState.Bootstrapped + this.state !== TorConnectState.Bootstrapped ); },
diff --git a/browser/modules/TorProcessService.jsm b/browser/modules/TorProcessService.jsm deleted file mode 100644 index 201e331b2806..000000000000 --- a/browser/modules/TorProcessService.jsm +++ /dev/null @@ -1,12 +0,0 @@ -"use strict"; - -var EXPORTED_SYMBOLS = ["TorProcessService"]; - -var TorProcessService = { - get isBootstrapDone() { - const svc = Cc["@torproject.org/torlauncher-process-service;1"].getService( - Ci.nsISupports - ).wrappedJSObject; - return svc.mIsBootstrapDone; - }, -}; diff --git a/browser/modules/TorProtocolService.jsm b/browser/modules/TorProtocolService.jsm deleted file mode 100644 index 6a1d6b94fff7..000000000000 --- a/browser/modules/TorProtocolService.jsm +++ /dev/null @@ -1,510 +0,0 @@ -// Copyright (c) 2021, The Tor Project, Inc. - -"use strict"; - -var EXPORTED_SYMBOLS = [ - "TorProtocolService", - "TorProcessStatus", - "TorTopics", - "TorBootstrapRequest", -]; - -const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); - -const { setTimeout, clearTimeout } = ChromeUtils.import( - "resource://gre/modules/Timer.jsm" -); - -const { TorLauncherUtil } = ChromeUtils.import( - "resource://torlauncher/modules/tl-util.jsm" -); - -// see tl-process.js -const TorProcessStatus = Object.freeze({ - Unknown: 0, - Starting: 1, - Running: 2, - Exited: 3, -}); - -/* tor-launcher observer topics */ -const TorTopics = Object.freeze({ - BootstrapStatus: "TorBootstrapStatus", - BootstrapError: "TorBootstrapError", - ProcessExited: "TorProcessExited", - LogHasWarnOrErr: "TorLogHasWarnOrErr", -}); - -/* Browser observer topis */ -const BrowserTopics = Object.freeze({ - ProfileAfterChange: "profile-after-change", -}); - -var TorProtocolService = { - _TorLauncherProtocolService: null, - _TorProcessService: null, - - // 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 primitive or array values - _settingsCache: new Map(), - - init() { - Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); - }, - - observe(subject, topic, data) { - if (topic === BrowserTopics.ProfileAfterChange) { - // we have to delay init'ing this or else the crypto service inits too early without a profile - // which breaks the password manager - this._TorLauncherProtocolService = Cc[ - "@torproject.org/torlauncher-protocol-service;1" - ].getService(Ci.nsISupports).wrappedJSObject; - this._TorProcessService = Cc[ - "@torproject.org/torlauncher-process-service;1" - ].getService(Ci.nsISupports).wrappedJSObject; - Services.obs.removeObserver(this, topic); - } - }, - - _typeof(aValue) { - switch (typeof aValue) { - case "boolean": - return "boolean"; - case "string": - return "string"; - case "object": - if (aValue == null) { - return "null"; - } else if (Array.isArray(aValue)) { - return "array"; - } - return "object"; - } - return "unknown"; - }, - - _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); - - const valueType = this._typeof(aValue); - switch (valueType) { - case "boolean": - case "string": - case "null": - return; - case "array": - for (const element of aValue) { - if (typeof element != "string") { - throw new Error( - `Setting '${aSetting}' array contains value of invalid type '${typeof element}'` - ); - } - } - return; - default: - throw new Error( - `Invalid object type received for setting '${aSetting}'` - ); - } - }, - - // takes a Map containing tor settings - // throws on error - async writeSettings(aSettingsObj) { - // only write settings that have changed - let newSettings = new Map(); - for (const [setting, value] of aSettingsObj) { - let saveSetting = false; - - // make sure we have valid data here - this._assertValidSetting(setting, value); - - if (!this._settingsCache.has(setting)) { - // no cached setting, so write - saveSetting = true; - } else { - const cachedValue = this._settingsCache.get(setting); - if (value != cachedValue) { - // compare arrays member-wise - if (Array.isArray(value) && Array.isArray(cachedValue)) { - if (value.length != cachedValue.length) { - saveSetting = true; - } else { - const arrayLength = value.length; - for (let i = 0; i < arrayLength; ++i) { - if (value[i] != cachedValue[i]) { - saveSetting = true; - break; - } - } - } - } else { - // some other different values - saveSetting = true; - } - } - } - - if (saveSetting) { - newSettings.set(setting, value); - } - } - - // only write if new setting to save - if (newSettings.size > 0) { - // convert settingsObject map to js object for torlauncher-protocol-service - let settingsObject = {}; - for (const [setting, value] of newSettings) { - settingsObject[setting] = value; - } - - let errorObject = {}; - if ( - !(await this._TorLauncherProtocolService.TorSetConfWithReply( - settingsObject, - errorObject - )) - ) { - throw new Error(errorObject.details); - } - - // save settings to cache after successfully writing to Tor - for (const [setting, value] of newSettings) { - this._settingsCache.set(setting, value); - } - } - }, - - async _readSetting(aSetting) { - this._assertValidSettingKey(aSetting); - let reply = await this._TorLauncherProtocolService.TorGetConf(aSetting); - if (this._TorLauncherProtocolService.TorCommandSucceeded(reply)) { - return reply.lineArray; - } - throw new Error(reply.lineArray.join("\n")); - }, - - async _readBoolSetting(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}` - ); - } - - let retval = lineArray[0]; - switch (retval) { - case "0": - return false; - case "1": - return true; - default: - throw new Error(`Expected boolean (1 or 0) but received '${retval}'`); - } - }, - - 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 _readStringArraySetting(aSetting) { - let lineArray = await this._readSetting(aSetting); - return lineArray; - }, - - 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; - }, - - async readStringArraySetting(aSetting) { - let value = await this._readStringArraySetting(aSetting); - this._settingsCache.set(aSetting, value); - return value; - }, - - // writes current tor settings to disk - async flushSettings() { - await this.sendCommand("SAVECONF"); - }, - - getLog(countObj) { - countObj = countObj || { value: 0 }; - let torLog = this._TorLauncherProtocolService.TorGetLog(countObj); - return torLog; - }, - - // true if we launched and control tor, false if using system tor - get ownsTorDaemon() { - return TorLauncherUtil.shouldStartAndOwnTor; - }, - - // Assumes `ownsTorDaemon` is true - isNetworkDisabled() { - const reply = TorProtocolService._TorLauncherProtocolService.TorGetConfBool( - "DisableNetwork", - true - ); - if ( - TorProtocolService._TorLauncherProtocolService.TorCommandSucceeded(reply) - ) { - return reply.retVal; - } - return true; - }, - - async enableNetwork() { - let settings = {}; - settings.DisableNetwork = false; - let errorObject = {}; - if ( - !(await this._TorLauncherProtocolService.TorSetConfWithReply( - settings, - errorObject - )) - ) { - throw new Error(errorObject.details); - } - }, - - async sendCommand(cmd) { - return this._TorLauncherProtocolService.TorSendCommand(cmd); - }, - - retrieveBootstrapStatus() { - return this._TorLauncherProtocolService.TorRetrieveBootstrapStatus(); - }, - - _GetSaveSettingsErrorMessage(aDetails) { - try { - return TorLauncherUtil.getSaveSettingsErrorMessage(aDetails); - } catch (e) { - console.log("GetSaveSettingsErrorMessage error", e); - return "Unexpected Error"; - } - }, - - async setConfWithReply(settings) { - let result = false; - const error = {}; - try { - result = await this._TorLauncherProtocolService.TorSetConfWithReply( - settings, - error - ); - } catch (e) { - console.log("TorSetConfWithReply error", e); - error.details = this._GetSaveSettingsErrorMessage(e.message); - } - return { result, error }; - }, - - isBootstrapDone() { - return this._TorProcessService.mIsBootstrapDone; - }, - - clearBootstrapError() { - return this._TorProcessService.TorClearBootstrapError(); - }, - - torBootstrapErrorOccurred() { - return this._TorProcessService.TorBootstrapErrorOccurred; - }, - - _torBootstrapDebugSetError() { - this._TorProcessService._TorSetBootstrapErrorForDebug(); - }, - - // Resolves to null if ok, or an error otherwise - async connect() { - const kTorConfKeyDisableNetwork = "DisableNetwork"; - const settings = {}; - settings[kTorConfKeyDisableNetwork] = false; - const { result, error } = await this.setConfWithReply(settings); - if (!result) { - return error; - } - try { - await this.sendCommand("SAVECONF"); - this.clearBootstrapError(); - this.retrieveBootstrapStatus(); - } catch (e) { - return error; - } - return null; - }, - - torLogHasWarnOrErr() { - return this._TorLauncherProtocolService.TorLogHasWarnOrErr; - }, - - async torStopBootstrap() { - // Tell tor to disable use of the network; this should stop the bootstrap - // process. - const kErrorPrefix = "Setting DisableNetwork=1 failed: "; - try { - let settings = {}; - settings.DisableNetwork = true; - const { result, error } = await this.setConfWithReply(settings); - if (!result) { - console.log( - `Error stopping bootstrap ${kErrorPrefix} ${error.details}` - ); - } - } catch (e) { - console.log(`Error stopping bootstrap ${kErrorPrefix} ${e}`); - } - this.retrieveBootstrapStatus(); - }, - - get torProcessStatus() { - if (this._TorProcessService) { - return this._TorProcessService.TorProcessStatus; - } - return TorProcessStatus.Unknown; - }, -}; -TorProtocolService.init(); - -// 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.torStopBootstrap(); - - 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.torStopBootstrap(); - 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() - .then(async err => { - if (!err) { - return; - } - - clearTimeout(this._timeoutID); - await TorProtocolService.torStopBootstrap(); - - const message = err.message; - const details = err.details; - if (this.onbootstraperror) { - this.onbootstraperror(message, details); - } - this._bootstrapPromiseResolve(false); - }) - .catch(err => { - // Currently, TorProtocolService.connect() should never throw - reject(err); - }); - }).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.torStopBootstrap(); - - this._bootstrapPromiseResolve(false); - } -} diff --git a/browser/modules/TorSettings.jsm b/browser/modules/TorSettings.jsm index 57a2a80c7d3f..4084ee71a1ce 100644 --- a/browser/modules/TorSettings.jsm +++ b/browser/modules/TorSettings.jsm @@ -11,14 +11,12 @@ var EXPORTED_SYMBOLS = [
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-const { TorProtocolService, TorProcessStatus } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" +const { TorMonitorService } = ChromeUtils.import( + "resource://gre/modules/TorMonitorService.jsm" +); +const { TorProtocolService } = ChromeUtils.import( + "resource://gre/modules/TorProtocolService.jsm" ); - -/* Browser observer topics */ -const BrowserTopics = Object.freeze({ - ProfileAfterChange: "profile-after-change", -});
/* tor-launcher observer topics */ const TorTopics = Object.freeze({ @@ -290,7 +288,7 @@ const TorSettings = (() => {
/* load or init our settings, and register observers */ init() { - if (TorProtocolService.ownsTorDaemon) { + if (TorMonitorService.ownsTorDaemon) { // if the settings branch exists, load settings from prefs if (Services.prefs.getBoolPref(TorSettingsPrefs.enabled, false)) { this.loadFromPrefs(); @@ -298,8 +296,11 @@ const TorSettings = (() => { // otherwise load defaults this._settings = this.defaultSettings(); } - Services.obs.addObserver(this, BrowserTopics.ProfileAfterChange); Services.obs.addObserver(this, TorTopics.ProcessIsReady); + + if (TorMonitorService.isRunning) { + handleProcessReady(); + } } },
@@ -316,12 +317,6 @@ const TorSettings = (() => { };
switch (topic) { - case BrowserTopics.ProfileAfterChange: - Services.obs.removeObserver(this, BrowserTopics.ProfileAfterChange); - if (TorProtocolService.torProcessStatus == TorProcessStatus.Running) { - await handleProcessReady(); - } - break; case TorTopics.ProcessIsReady: Services.obs.removeObserver(this, TorTopics.ProcessIsReady); await handleProcessReady(); diff --git a/browser/modules/moz.build b/browser/modules/moz.build index 98caa91ee6f4..b9158588b812 100644 --- a/browser/modules/moz.build +++ b/browser/modules/moz.build @@ -147,8 +147,6 @@ EXTRA_JS_MODULES += [ "TabsList.jsm", "TabUnloader.jsm", "TorConnect.jsm", - "TorProcessService.jsm", - "TorProtocolService.jsm", "TorSettings.jsm", "TorStrings.jsm", "TransientPrefs.jsm", diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm index 4afa7f118a72..f99dd538ec7c 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.jsm +++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm @@ -20,8 +20,6 @@ MainProcessSingleton.prototype = { // Imported for side-effects. ChromeUtils.import("resource://gre/modules/CustomElementsListener.jsm");
- // FIXME: Is this import really necessary? - ChromeUtils.import("resource:///modules/TorSettings.jsm"); ChromeUtils.import("resource:///modules/TorConnect.jsm");
Services.ppmm.loadProcessScript( diff --git a/toolkit/components/tor-launcher/TorStartupService.jsm b/toolkit/components/tor-launcher/TorStartupService.jsm index 9c27b34b2f6f..539c599aec62 100644 --- a/toolkit/components/tor-launcher/TorStartupService.jsm +++ b/toolkit/components/tor-launcher/TorStartupService.jsm @@ -22,6 +22,17 @@ ChromeUtils.defineModuleGetter( "resource://gre/modules/TorProtocolService.jsm" );
+ChromeUtils.defineModuleGetter( + this, + "TorConnect", + "resource:///modules/TorConnect.jsm" +); +ChromeUtils.defineModuleGetter( + this, + "TorSettings", + "resource:///modules/TorSettings.jsm" +); + /* Browser observer topis */ const BrowserTopics = Object.freeze({ ProfileAfterChange: "profile-after-change", @@ -53,6 +64,9 @@ class TorStartupService { await TorProtocolService.init(); TorMonitorService.init();
+ TorSettings.init(); + TorConnect.init(); + try { TorLauncherUtil.removeMeekAndMoatHelperProfiles(); } catch (e) {
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 c3726ddc7c054f8803be72d4fb26ae20983a1411 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:18:40 2022 +0200
fixup! Bug 27476: Implement about:torconnect captive portal within Tor Browser
Do not include TorConnect.jsm in MainProcessSingleton.jsm --- toolkit/components/processsingleton/MainProcessSingleton.jsm | 2 -- 1 file changed, 2 deletions(-)
diff --git a/toolkit/components/processsingleton/MainProcessSingleton.jsm b/toolkit/components/processsingleton/MainProcessSingleton.jsm index f99dd538ec7c..4f800b93fbce 100644 --- a/toolkit/components/processsingleton/MainProcessSingleton.jsm +++ b/toolkit/components/processsingleton/MainProcessSingleton.jsm @@ -20,8 +20,6 @@ MainProcessSingleton.prototype = { // Imported for side-effects. ChromeUtils.import("resource://gre/modules/CustomElementsListener.jsm");
- ChromeUtils.import("resource:///modules/TorConnect.jsm"); - Services.ppmm.loadProcessScript( "chrome://global/content/process-content.js", true
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 eeb101368c6e426008de31e00332e4f8bba67c9e Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:19:47 2022 +0200
fixup! Bug 4234: Use the Firefox Update Process for Tor Browser.
Bug 40933: Moved tor-launcher to tor-browser.git --- toolkit/mozapps/update/UpdateService.jsm | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm index 6ebbc1e0d0c6..d261a2d23d6c 100644 --- a/toolkit/mozapps/update/UpdateService.jsm +++ b/toolkit/mozapps/update/UpdateService.jsm @@ -13,15 +13,15 @@ const { AUSTLMY } = ChromeUtils.import( "resource://gre/modules/UpdateTelemetry.jsm" );
-const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" +const { TorMonitorService } = ChromeUtils.import( + "resource://gre/modules/TorMonitorService.jsm" );
function _shouldRegisterBootstrapObserver(errorCode) { return ( errorCode == PROXY_SERVER_CONNECTION_REFUSED && - !TorProtocolService.isBootstrapDone() && - TorProtocolService.ownsTorDaemon + !TorMonitorService.isBootstrapDone() && + TorMonitorService.ownsTorDaemon ); }
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 91abcc7a6a7f2204ae4e97a0b591afc51adc6624 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Mon Oct 10 15:20:56 2022 +0200
fixup! Bug 31286: Implementation of bridge, proxy, and firewall settings in about:preferences#connection
Bug 40933: Move tor-launcher to tor-browser.git --- browser/components/torpreferences/content/connectionPane.js | 11 +++++++---- browser/components/torpreferences/content/torLogDialog.jsm | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-)
diff --git a/browser/components/torpreferences/content/connectionPane.js b/browser/components/torpreferences/content/connectionPane.js index d89f42ca7613..c1a069c8cd49 100644 --- a/browser/components/torpreferences/content/connectionPane.js +++ b/browser/components/torpreferences/content/connectionPane.js @@ -13,8 +13,11 @@ const { TorBridgeSource, } = ChromeUtils.import("resource:///modules/TorSettings.jsm");
+const { TorMonitorService } = ChromeUtils.import( + "resource://gre/modules/TorMonitorService.jsm" +); const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" + "resource://gre/modules/TorProtocolService.jsm" );
const { @@ -198,7 +201,7 @@ const gConnectionPane = (function() { TorConnect.state === TorConnectState.Configuring ) { // set messagebox style and text - if (TorProtocolService.torBootstrapErrorOccurred()) { + if (TorMonitorService.bootstrapErrorOccurred) { messageBox.parentNode.style.display = null; messageBox.className = "error"; messageBoxMessage.innerText = TorStrings.torConnect.tryAgainMessage; @@ -396,7 +399,7 @@ const gConnectionPane = (function() { this._showAutoconfiguration = () => { if ( !TorConnect.shouldShowTorConnect || - !TorProtocolService.torBootstrapErrorOccurred() + !TorMonitorService.bootstrapErrorOccurred ) { locationGroup.setAttribute("hidden", "true"); return; @@ -945,7 +948,7 @@ const gConnectionPane = (function() {
// whether the page should be present in about:preferences get enabled() { - return TorProtocolService.ownsTorDaemon; + return TorMonitorService.ownsTorDaemon; },
// diff --git a/browser/components/torpreferences/content/torLogDialog.jsm b/browser/components/torpreferences/content/torLogDialog.jsm index 94a57b9b165e..bf50cb801e10 100644 --- a/browser/components/torpreferences/content/torLogDialog.jsm +++ b/browser/components/torpreferences/content/torLogDialog.jsm @@ -6,8 +6,8 @@ const { setTimeout, clearTimeout } = ChromeUtils.import( "resource://gre/modules/Timer.jsm" );
-const { TorProtocolService } = ChromeUtils.import( - "resource:///modules/TorProtocolService.jsm" +const { TorMonitorService } = ChromeUtils.import( + "resource://gre/modules/TorMonitorService.jsm" ); const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
@@ -56,7 +56,7 @@ class TorLogDialog { }, RESTORE_TIME); });
- this._logTextarea.value = TorProtocolService.getLog(); + this._logTextarea.value = TorMonitorService.getLog(); }
init(window, aDialog) {
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 0a0945855cb7fe1e4f5ade2d4b4319bd83ad0f40 Author: Richard Pospesel richard@torproject.org AuthorDate: Mon Oct 10 19:30:54 2022 +0000
fixup! Bug 10760: Integrate TorButton to TorBrowser core
Bug 40933: Move tor-launcher to tor-browser.git --- toolkit/torproject/torbutton | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/toolkit/torproject/torbutton b/toolkit/torproject/torbutton index 03fc12014cf3..d96a88641bf9 160000 --- a/toolkit/torproject/torbutton +++ b/toolkit/torproject/torbutton @@ -1 +1 @@ -Subproject commit 03fc12014cf3fb770086223ad009335c2c73d842 +Subproject commit d96a88641bf94ae926bdf326978ce799ddbedb89
tbb-commits@lists.torproject.org