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