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/9722ca2...