commit 844d14a86646f2358fccdb211be6b4b8d82ff7ad
parent dbdbacab52305ddd875b1773e868e8306623b42a
Author: Pier Angelo Vendrame <pierov@torproject.org>
Date: Mon, 10 Oct 2022 15:13:04 +0200
TB 40933: Add tor-launcher functionality
Bug 41926: Reimplement the control port
Diffstat:
15 files changed, 4414 insertions(+), 0 deletions(-)
diff --git a/browser/app/profile/000-tor-browser.js b/browser/app/profile/000-tor-browser.js
@@ -131,3 +131,5 @@ pref("extensions.torlauncher.bridgedb_reflector", "https://bespoke-strudel-c243c
pref("extensions.torlauncher.moat_service", "https://bridges.torproject.org/moat");
// Log levels
+pref("browser.tor_provider.log_level", "Warn");
+pref("browser.tor_provider.cp_log_level", "Warn");
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in
@@ -235,6 +235,9 @@
@RESPATH@/chrome/torbutton.manifest
@RESPATH@/chrome/torbutton/*
+; Tor integration
+@RESPATH@/components/tor-launcher.manifest
+
; [DevTools Startup Files]
@RESPATH@/browser/chrome/devtools-startup@JAREXT@
@RESPATH@/browser/chrome/devtools-startup.manifest
diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build
@@ -85,6 +85,7 @@ DIRS += [
"thumbnails",
"timermanager",
"tooltiptext",
+ "tor-launcher",
"typeaheadfind",
"utils",
"url-classifier",
diff --git a/toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs b/toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs
@@ -0,0 +1,145 @@
+import { setTimeout, clearTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+ TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
+
+const log = console.createInstance({
+ maxLogLevel: "Info",
+ prefix: "TorBootstrapRequest",
+});
+
+/**
+ * This class encapsulates the observer register/unregister logic to provide an
+ * XMLHttpRequest-like API to bootstrap tor.
+ * TODO: Remove this class, and move its logic inside the TorProvider.
+ */
+export class TorBootstrapRequest {
+ // number of ms to wait before we abandon the bootstrap attempt
+ // a value of 0 implies we never wait
+ timeout = 0;
+
+ // callbacks for bootstrap process status updates
+ onbootstrapstatus = (_progress, _status) => {};
+ onbootstrapcomplete = () => {};
+ onbootstraperror = _error => {};
+
+ // internal resolve() method for bootstrap
+ #bootstrapPromiseResolve = null;
+ #bootstrapPromise = null;
+ #timeoutID = null;
+
+ observe(subject, topic) {
+ const obj = subject?.wrappedJSObject;
+ switch (topic) {
+ case lazy.TorProviderTopics.BootstrapStatus: {
+ const progress = obj.PROGRESS;
+ if (this.onbootstrapstatus) {
+ const status = obj.TAG;
+ this.onbootstrapstatus(progress, status);
+ }
+ if (progress === 100) {
+ if (this.onbootstrapcomplete) {
+ this.onbootstrapcomplete();
+ }
+ this.#bootstrapPromiseResolve(true);
+ clearTimeout(this.#timeoutID);
+ this.#timeoutID = null;
+ }
+
+ break;
+ }
+ case lazy.TorProviderTopics.BootstrapError: {
+ log.info("TorBootstrapRequest: observerd TorBootstrapError", obj);
+ this.#stop(obj);
+ break;
+ }
+ }
+ }
+
+ // resolves 'true' if bootstrap succeeds, false otherwise
+ bootstrap() {
+ if (this.#bootstrapPromise) {
+ return this.#bootstrapPromise;
+ }
+
+ this.#bootstrapPromise = new Promise(resolve => {
+ this.#bootstrapPromiseResolve = resolve;
+
+ // register ourselves to listen for bootstrap events
+ Services.obs.addObserver(this, lazy.TorProviderTopics.BootstrapStatus);
+ Services.obs.addObserver(this, lazy.TorProviderTopics.BootstrapError);
+
+ // optionally cancel bootstrap after a given timeout
+ if (this.timeout > 0) {
+ this.#timeoutID = setTimeout(() => {
+ this.#timeoutID = null;
+ this.#stop(
+ new Error(
+ `Bootstrap attempt abandoned after waiting ${this.timeout} ms`
+ )
+ );
+ }, this.timeout);
+ }
+
+ // Wait for bootstrapping to begin and maybe handle error.
+ // Notice that we do not resolve the promise here in case of success, but
+ // we do it from the BootstrapStatus observer.
+ // NOTE: After TorProviderBuilder.build resolves, TorProvider.init will
+ // have completed. In particular, assuming no errors, the TorSettings will
+ // have been initialised and passed on to the provider via
+ // TorProvider.writeSettings. Therefore we should be safe to immediately
+ // call `connect` using the latest user settings.
+ lazy.TorProviderBuilder.build()
+ .then(provider => provider.connect())
+ .catch(err => {
+ this.#stop(err);
+ });
+ }).finally(() => {
+ // and remove ourselves once bootstrap is resolved
+ Services.obs.removeObserver(this, lazy.TorProviderTopics.BootstrapStatus);
+ Services.obs.removeObserver(this, lazy.TorProviderTopics.BootstrapError);
+ this.#bootstrapPromise = null;
+ });
+
+ return this.#bootstrapPromise;
+ }
+
+ async cancel() {
+ await this.#stop();
+ }
+
+ // Internal implementation. Do not use directly, but call cancel, instead.
+ async #stop(error) {
+ // first stop our bootstrap timeout before handling the error
+ if (this.#timeoutID !== null) {
+ clearTimeout(this.#timeoutID);
+ this.#timeoutID = null;
+ }
+
+ let provider;
+ try {
+ provider = await lazy.TorProviderBuilder.build();
+ } catch {
+ // This was probably the error that lead to stop in the first place.
+ // No need to continue propagating it.
+ }
+ try {
+ await provider?.stopBootstrap();
+ } catch (e) {
+ console.error("Failed to stop the bootstrap.", e);
+ if (!error) {
+ error = e;
+ }
+ }
+
+ if (this.onbootstraperror && error) {
+ this.onbootstraperror(error);
+ }
+
+ this.#bootstrapPromiseResolve(false);
+ }
+}
diff --git a/toolkit/components/tor-launcher/TorControlPort.sys.mjs b/toolkit/components/tor-launcher/TorControlPort.sys.mjs
@@ -0,0 +1,1348 @@
+/* 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/. */
+
+import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
+
+const logger = console.createInstance({
+ maxLogLevelPref: "browser.tor_provider.cp_log_level",
+ prefix: "TorControlPort",
+});
+
+/**
+ * A wrapper around XPCOM sockets and buffers to handle streams in a standard
+ * async JS fashion.
+ * This class can handle both Unix sockets and TCP sockets.
+ */
+class AsyncSocket {
+ /**
+ * The output stream used for write operations.
+ *
+ * @type {nsIAsyncOutputStream}
+ */
+ #outputStream;
+ /**
+ * The output stream can only have one registered callback at a time, so
+ * multiple writes need to be queued up (see nsIAsyncOutputStream.idl).
+ * Every item is associated with a promise we returned in write, and it will
+ * resolve it or reject it when called by the output stream.
+ *
+ * @type {nsIOutputStreamCallback[]}
+ */
+ #outputQueue = [];
+ /**
+ * The input stream.
+ *
+ * @type {nsIAsyncInputStream}
+ */
+ #inputStream;
+ /**
+ * An input stream adapter that makes reading from scripts easier.
+ *
+ * @type {nsIScriptableInputStream}
+ */
+ #scriptableInputStream;
+ /**
+ * The queue of callbacks to be used when we receive data.
+ * Every item is associated with a promise we returned in read, and it will
+ * resolve it or reject it when called by the input stream.
+ *
+ * @type {nsIInputStreamCallback[]}
+ */
+ #inputQueue = [];
+
+ /**
+ * Connect to a Unix socket. Not available on Windows.
+ *
+ * @param {nsIFile} ipcFile The path to the Unix socket to connect to.
+ * @returns {AsyncSocket}
+ */
+ static fromIpcFile(ipcFile) {
+ const sts = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+ ].getService(Ci.nsISocketTransportService);
+ const socket = new AsyncSocket();
+ const transport = sts.createUnixDomainTransport(ipcFile);
+ socket.#createStreams(transport);
+ return socket;
+ }
+
+ /**
+ * Connect to a TCP socket.
+ *
+ * @param {string} host The hostname to connect the TCP socket to.
+ * @param {number} port The port to connect the TCP socket to.
+ * @returns {AsyncSocket}
+ */
+ static fromSocketAddress(host, port) {
+ const sts = Cc[
+ "@mozilla.org/network/socket-transport-service;1"
+ ].getService(Ci.nsISocketTransportService);
+ const socket = new AsyncSocket();
+ const transport = sts.createTransport([], host, port, null, null);
+ socket.#createStreams(transport);
+ return socket;
+ }
+
+ #createStreams(socketTransport) {
+ const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
+ this.#outputStream = socketTransport
+ .openOutputStream(OPEN_UNBUFFERED, 1, 1)
+ .QueryInterface(Ci.nsIAsyncOutputStream);
+
+ this.#inputStream = socketTransport
+ .openInputStream(OPEN_UNBUFFERED, 1, 1)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ this.#scriptableInputStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ this.#scriptableInputStream.init(this.#inputStream);
+ }
+
+ /**
+ * Asynchronously write string to underlying socket.
+ *
+ * When write is called, we create a new promise and queue it on the output
+ * queue. If it is the only element in the queue, we ask the output stream to
+ * run it immediately.
+ * Otherwise, the previous item of the queue will run it after it finishes.
+ *
+ * @param {string} str The string to write to the socket. The underlying
+ * implementation should convert JS strings (UTF-16) into UTF-8 strings.
+ * See also write nsIOutputStream (the first argument is a string, not a
+ * wstring).
+ * @returns {Promise<number>} The number of written bytes
+ */
+ async write(str) {
+ return new Promise((resolve, reject) => {
+ // Asynchronously wait for the stream to be writable (or closed) if we
+ // have any pending requests.
+ const tryAsyncWait = () => {
+ if (this.#outputQueue.length) {
+ this.#outputStream.asyncWait(
+ this.#outputQueue.at(0), // next request
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ }
+ };
+
+ // Implement an nsIOutputStreamCallback: write the string once possible,
+ // and then start running the following queue item, if any.
+ this.#outputQueue.push({
+ onOutputStreamReady: () => {
+ try {
+ const bytesWritten = this.#outputStream.write(str, str.length);
+
+ // Remove this callback object from queue, as it is now completed.
+ this.#outputQueue.shift();
+
+ // Queue the next request if there is one.
+ tryAsyncWait();
+
+ // Finally, resolve the promise.
+ resolve(bytesWritten);
+ } catch (err) {
+ // Reject the promise on error.
+ reject(err);
+ }
+ },
+ });
+
+ // Length 1 imples that there is no in-flight asyncWait, so we may
+ // immediately follow through on this write.
+ if (this.#outputQueue.length === 1) {
+ tryAsyncWait();
+ }
+ });
+ }
+
+ /**
+ * Asynchronously read string from underlying socket and return it.
+ *
+ * When read is called, we create a new promise and queue it on the input
+ * queue. If it is the only element in the queue, we ask the input stream to
+ * run it immediately.
+ * Otherwise, the previous item of the queue will run it after it finishes.
+ *
+ * This function is expected to throw when the underlying socket has been
+ * closed.
+ *
+ * @returns {Promise<string>} The read string
+ */
+ async read() {
+ return new Promise((resolve, reject) => {
+ const tryAsyncWait = () => {
+ if (this.#inputQueue.length) {
+ this.#inputStream.asyncWait(
+ this.#inputQueue.at(0), // next input request
+ 0,
+ 0,
+ Services.tm.currentThread
+ );
+ }
+ };
+
+ this.#inputQueue.push({
+ onInputStreamReady: () => {
+ try {
+ if (!this.#scriptableInputStream.available()) {
+ // This means EOF, but not closed yet. However, arriving at EOF
+ // should be an error condition for us, since we are in a socket,
+ // and EOF should mean peer disconnected.
+ // If the stream has been closed, this function itself should
+ // throw.
+ reject(
+ new Error("onInputStreamReady called without available bytes.")
+ );
+ return;
+ }
+
+ // Read our string from input stream.
+ const str = this.#scriptableInputStream.read(
+ this.#scriptableInputStream.available()
+ );
+
+ // Remove this callback object from queue now that we have read.
+ this.#inputQueue.shift();
+
+ // Start waiting for incoming data again if the reading queue is not
+ // empty.
+ tryAsyncWait();
+
+ // Finally resolve the promise.
+ resolve(str);
+ } catch (err) {
+ // E.g., we received a NS_BASE_STREAM_CLOSED because the socket was
+ // closed.
+ reject(err);
+ }
+ },
+ });
+
+ // Length 1 imples that there is no in-flight asyncWait, so we may
+ // immediately follow through on this read.
+ if (this.#inputQueue.length === 1) {
+ tryAsyncWait();
+ }
+ });
+ }
+
+ /**
+ * Close the streams.
+ */
+ close() {
+ this.#outputStream.close();
+ this.#inputStream.close();
+ }
+}
+
+/**
+ * @typedef Command
+ * @property {string} commandString The string to send over the control port
+ * @property {Function} resolve The function to resolve the promise with the
+ * response we got on the control port
+ * @property {Function} reject The function to reject the promise associated to
+ * the command
+ */
+/**
+ * The ID of a circuit.
+ * From control-spec.txt:
+ * CircuitID = 1*16 IDChar
+ * IDChar = ALPHA / DIGIT
+ * Currently, Tor only uses digits, but this may change.
+ *
+ * @typedef {string} CircuitID
+ */
+/**
+ * The ID of a stream.
+ * From control-spec.txt:
+ * CircuitID = 1*16 IDChar
+ * IDChar = ALPHA / DIGIT
+ * Currently, Tor only uses digits, but this may change.
+ *
+ * @typedef {string} StreamID
+ */
+/**
+ * The fingerprint of a node.
+ * From control-spec.txt:
+ * Fingerprint = "$" 40*HEXDIG
+ * However, we do not keep the $ in our structures.
+ *
+ * @typedef {string} NodeFingerprint
+ */
+/**
+ * @typedef {object} CircuitInfo
+ * @property {CircuitID} id The ID of a circuit
+ * @property {NodeFingerprint[]} nodes List of node fingerprints
+ */
+/**
+ * @typedef {object} Bridge
+ * @property {string} transport The transport of the bridge, or vanilla if not
+ * specified
+ * @property {string} addr The IP address and port of the bridge
+ * @property {NodeFingerprint} id The fingerprint of the bridge
+ * @property {string} args Optional arguments passed to the bridge
+ */
+/**
+ * @typedef {object} PTInfo The information about a pluggable transport
+ * @property {string[]} transports An array with all the transports supported by
+ * this configuration
+ * @property {string} type Either socks4, socks5 or exec
+ * @property {string} [ip] The IP address of the proxy (only for socks4 and
+ * socks5)
+ * @property {integer} [port] The port of the proxy (only for socks4 and socks5)
+ * @property {string} [pathToBinary] Path to the binary that is run (only for
+ * exec)
+ * @property {string} [options] Optional options passed to the binary (only for
+ * exec)
+ */
+/**
+ * @typedef {object} SocksListener
+ * @property {string} [ipcPath] path to a Unix socket to use for an IPC proxy
+ * @property {string} [host] The host to connect for a TCP proxy
+ * @property {number} [port] The port number to use for a TCP proxy
+ */
+/**
+ * @typedef {object} OnionAuthKeyInfo
+ * @property {string} address The address of the onion service
+ * @property {string} typeAndKey Onion service key and type of key, as
+ * `type:base64-private-key`
+ * @property {string} Flags Additional flags, such as Permanent
+ */
+/**
+ * @callback EventCallback A callback to receive messages from the control
+ * port.
+ * @param {string} message The message to handle
+ */
+
+/**
+ * This is a custom error thrown when we receive an error response over the
+ * control port.
+ *
+ * It includes the command that caused it, the error code and the raw message
+ * sent by the tor daemon.
+ */
+class TorProtocolError extends Error {
+ constructor(command, reply) {
+ super(`${command} -> ${reply}`);
+ this.name = "TorProtocolError";
+ const info = reply.match(/(?<code>\d{3})(?:\s(?<message>.+))?/);
+ this.torStatusCode = info.groups.code;
+ if (info.groups.message) {
+ this.torMessage = info.groups.message;
+ }
+ }
+}
+
+/**
+ * This class implements a JavaScript API around some of the commands and
+ * notifications that can be sent and received with tor's control port protocol.
+ */
+export class TorController {
+ /**
+ * The socket to write to the control port.
+ *
+ * @type {AsyncSocket}
+ */
+ #socket;
+
+ /**
+ * Data we received on a read but that was not a complete line (missing a
+ * final CRLF). We will prepend it to the next read.
+ *
+ * @type {string}
+ */
+ #pendingData = "";
+ /**
+ * The lines we received and are still queued for being evaluated.
+ *
+ * @type {string[]}
+ */
+ #pendingLines = [];
+ /**
+ * The commands that need to be run or receive a response.
+ *
+ * NOTE: This must be in the order with the last requested command at the end
+ * of the queue.
+ *
+ * @type {Command[]}
+ */
+ #commandQueue = [];
+
+ /**
+ * The event handler.
+ *
+ * @type {TorEventHandler}
+ */
+ #eventHandler;
+
+ /**
+ * Connect to a control port over a Unix socket.
+ * Not available on Windows.
+ *
+ * @param {nsIFile} ipcFile The path to the Unix socket to connect to
+ * @param {TorEventHandler} eventHandler The event handler to use for
+ * asynchronous notifications
+ * @returns {TorController}
+ */
+ static fromIpcFile(ipcFile, eventHandler) {
+ return new TorController(AsyncSocket.fromIpcFile(ipcFile), eventHandler);
+ }
+
+ /**
+ * Connect to a control port over a TCP socket.
+ *
+ * @param {string} host The hostname to connect to
+ * @param {number} port The port to connect the to
+ * @param {TorEventHandler} eventHandler The event handler to use for
+ * asynchronous notifications
+ * @returns {TorController}
+ */
+ static fromSocketAddress(host, port, eventHandler) {
+ return new TorController(
+ AsyncSocket.fromSocketAddress(host, port),
+ eventHandler
+ );
+ }
+
+ /**
+ * Construct the controller and start the message pump.
+ * The class should not be constructed directly, but through static methods.
+ * However, this is public because JavaScript does not support private
+ * constructors.
+ *
+ * @private
+ * @param {AsyncSocket} socket The socket to use
+ * @param {TorEventHandler} eventHandler The event handler to use for
+ * asynchronous notifications
+ */
+ constructor(socket, eventHandler) {
+ this.#socket = socket;
+ this.#eventHandler = eventHandler;
+ this.#startMessagePump();
+ }
+
+ // Socket and communication handling
+
+ /**
+ * Return the next line in the queue. If there is not any, block until one is
+ * read (or until a communication error happens, including the underlying
+ * socket being closed while it was still waiting for data).
+ * Any letfovers will be prepended to the next read.
+ *
+ * @returns {Promise<string>} A line read over the socket
+ */
+ async #readLine() {
+ // Keep reading from socket until we have at least a full line to return.
+ while (!this.#pendingLines.length) {
+ if (!this.#socket) {
+ throw new Error(
+ "Read interrupted because the control socket is not available anymore"
+ );
+ }
+ // Read data from our socket and split on newline tokens.
+ // This might still throw when the socket has been closed.
+ this.#pendingData += await this.#socket.read();
+ const lines = this.#pendingData.split("\r\n");
+ // The last line will either be empty string, or a partial read of a
+ // response/event so save it off for the next socket read.
+ this.#pendingData = lines.pop();
+ // Copy remaining full lines to our pendingLines list.
+ this.#pendingLines = this.#pendingLines.concat(lines);
+ }
+ return this.#pendingLines.shift();
+ }
+
+ /**
+ * Blocks until an entire message is ready and returns it.
+ * This function does a rudimentary parsing of the data only to handle
+ * multi-line responses.
+ *
+ * @returns {Promise<string>} The read message (without the final CRLF)
+ */
+ async #readMessage() {
+ // Whether we are searching for the end of a multi-line values.
+ // See control-spec section 3.9.
+ let handlingMultlineValue = false;
+ let endOfMessageFound = false;
+ const message = [];
+
+ do {
+ const line = await this.#readLine();
+ message.push(line);
+
+ if (handlingMultlineValue) {
+ // look for end of multiline
+ if (line === ".") {
+ handlingMultlineValue = false;
+ }
+ } else {
+ // 'Multiline values' are possible. We avoid interrupting one by
+ // detecting it and waiting for a terminating "." on its own line.
+ // (See control-spec section 3.9 and
+ // https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/16990#note_2625464).
+ // Ensure this is the first line of a new message
+ // eslint-disable-next-line no-lonely-if
+ if (message.length === 1 && line.match(/^\d\d\d\+.+?=$/)) {
+ handlingMultlineValue = true;
+ }
+ // Look for end of message (notice the space character at end of the
+ // regex!).
+ else if (line.match(/^\d\d\d /)) {
+ if (message.length === 1) {
+ endOfMessageFound = true;
+ } else {
+ const firstReplyCode = message[0].substring(0, 3);
+ const lastReplyCode = line.substring(0, 3);
+ endOfMessageFound = firstReplyCode === lastReplyCode;
+ }
+ }
+ }
+ } while (!endOfMessageFound);
+
+ // join our lines back together to form one message
+ return message.join("\r\n");
+ }
+
+ /**
+ * Handles a message that was received as a reply to a command (i.e., all the
+ * messages that are not async notification messages starting with 650).
+ *
+ * @param {string} message The message to handle
+ */
+ #handleCommandReply(message) {
+ const cmd = this.#commandQueue.shift();
+ // We resolve also for messages that are failures for sure. The commands
+ // should always check the output.
+ cmd.resolve(message);
+
+ // send next command if one is available
+ if (this.#commandQueue.length) {
+ this.#writeNextCommand();
+ }
+ }
+
+ /**
+ * Read messages on the socket and routed them to a dispatcher until the
+ * socket is open or some error happens (including the underlying socket being
+ * closed).
+ */
+ async #startMessagePump() {
+ try {
+ // This while is inside the try block because it is very likely that it
+ // will be broken by a NS_BASE_STREAM_CLOSED exception, rather than by its
+ // condition becoming false.
+ while (this.#socket) {
+ const message = await this.#readMessage();
+ try {
+ if (message.startsWith("650")) {
+ this.#handleNotification(message);
+ } else {
+ this.#handleCommandReply(message);
+ }
+ } catch (err) {
+ // E.g., if a notification handler fails. Without this internal
+ // try/catch we risk of closing the connection while not actually
+ // needed.
+ logger.error("Caught an exception while handling a message", err);
+ }
+ }
+ } catch (err) {
+ logger.debug("Caught an exception, closing the control port", err);
+ try {
+ this.#close(err);
+ } catch (ec) {
+ logger.error(
+ "Caught another error while closing the control socket.",
+ ec
+ );
+ }
+ }
+ }
+
+ /**
+ * Start running the first available command in the queue.
+ * To be called when the previous one has finished running.
+ * This makes sure to avoid conflicts when using the control port.
+ */
+ #writeNextCommand() {
+ const cmd = this.#commandQueue[0];
+ this.#socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
+ }
+
+ /**
+ * Send a command over the control port.
+ * This function returns only when it receives a complete message over the
+ * control port. This class does some rudimentary parsing to check wheter it
+ * needs to handle multi-line messages.
+ *
+ * @param {string} commandString The command to send
+ * @returns {Promise<string>} The message sent by the control port. The return
+ * value should never be an empty string (even though it will not include the
+ * final CRLF).
+ */
+ async #sendCommand(commandString) {
+ if (!this.#socket) {
+ throw new Error("ControlSocket not open");
+ }
+
+ // this promise is resolved either in #handleCommandReply, or in
+ // #startMessagePump (on stream error)
+ return new Promise((resolve, reject) => {
+ const command = {
+ commandString,
+ resolve,
+ reject,
+ };
+ this.#commandQueue.push(command);
+ if (this.#commandQueue.length === 1) {
+ this.#writeNextCommand();
+ }
+ });
+ }
+
+ /**
+ * Send a simple command whose response is expected to be simply a "250 OK".
+ * The function will not return a reply, but will throw if an unexpected one
+ * is received.
+ *
+ * @param {string} command The command to send
+ */
+ async #sendCommandSimple(command) {
+ const reply = await this.#sendCommand(command);
+ if (!/^250 OK\s*$/i.test(reply)) {
+ throw new TorProtocolError(command, reply);
+ }
+ }
+
+ /**
+ * Reject all the commands that are still in queue and close the control
+ * socket.
+ *
+ * @param {object?} reason An error object used to pass a more specific
+ * rejection reason to the commands that are still queued.
+ */
+ #close(reason) {
+ logger.info("Closing the control port", reason);
+ const error = new Error(
+ "The control socket has been closed" +
+ (reason ? `: ${reason.message}` : "")
+ );
+ const commands = this.#commandQueue;
+ this.#commandQueue = [];
+ for (const cmd of commands) {
+ cmd.reject(error);
+ }
+ try {
+ this.#socket?.close();
+ } finally {
+ this.#socket = null;
+ }
+ }
+
+ /**
+ * Closes the socket connected to the control port.
+ */
+ close() {
+ this.#close(null);
+ }
+
+ /**
+ * Tells whether the underlying socket is still open.
+ *
+ * @returns {boolean}
+ */
+ get isOpen() {
+ return !!this.#socket;
+ }
+
+ /**
+ * Authenticate to the tor daemon.
+ * Notice that a failure in the authentication makes the connection close.
+ *
+ * @param {Uint8Array} password The password for the control port, as an array
+ * of bytes
+ */
+ async authenticate(password) {
+ const passwordString = Array.from(password ?? [], b =>
+ b.toString(16).padStart(2, "0")
+ ).join("");
+ await this.#sendCommandSimple(`authenticate ${passwordString}`);
+ }
+
+ // Information
+
+ /**
+ * Sends a GETINFO for a single key.
+ * control-spec.txt says "one ReplyLine is sent for each requested value", so,
+ * we expect to receive only one line starting with `250-keyword=`, or one
+ * line starting with `250+keyword=` (in which case we will match until a
+ * period).
+ * This function could be possibly extended to handle several keys at once,
+ * but we currently do not need this functionality, so we preferred keeping
+ * the function simpler.
+ *
+ * @param {string} key The key to get value for
+ * @returns {Promise<string>} The string we received (only the value, without
+ * the key). We do not do any additional parsing on it
+ */
+ async #getInfo(key) {
+ this.#expectString(key);
+ const cmd = `GETINFO ${key}`;
+ const reply = await this.#sendCommand(cmd);
+ const match =
+ reply.match(/^250-([^=]+)=(.*)$/m) ||
+ reply.match(/^250\+([^=]+)=\r?\n(.*?)\r?\n^\.\r?\n^250 OK\s*$/ms);
+ if (!match || match[1] !== key) {
+ throw new TorProtocolError(cmd, reply);
+ }
+ return match[2];
+ }
+
+ /**
+ * Ask Tor its bootstrap phase.
+ *
+ * @returns {object} An object with the bootstrap information received from
+ * Tor. Its keys might vary, depending on the input
+ */
+ async getBootstrapPhase() {
+ return this.#parseBootstrapStatus(
+ await this.#getInfo("status/bootstrap-phase")
+ );
+ }
+
+ /**
+ * Get the IPv4 and optionally IPv6 addresses of an onion router.
+ *
+ * @param {NodeFingerprint} id The fingerprint of the node the caller is
+ * interested in
+ * @returns {string[]} The IP addresses (one IPv4 and optionally an IPv6)
+ */
+ async getNodeAddresses(id) {
+ this.#expectString(id, "id");
+ const reply = await this.#getInfo(`ns/id/${id}`);
+ // See dir-spec.txt.
+ // r nickname identity digest publication IP OrPort DirPort
+ const rLine = reply.match(/^r\s+(.*)$/m);
+ const v4 = rLine ? rLine[1].split(/\s+/) : [];
+ // Tor should already reply with a 552 when a relay cannot be found.
+ // Also, publication is a date with a space inside, so it is counted twice.
+ if (!rLine || v4.length !== 8) {
+ throw new Error(`Received an invalid node information: ${reply}`);
+ }
+ const addresses = [v4[5]];
+ // a address:port
+ // dir-spec.txt also states only the first one should be taken
+ const v6 = reply.match(/^a\s+\[([0-9a-fA-F:]+)\]:\d{1,5}$/m);
+ if (v6) {
+ addresses.push(v6[1]);
+ }
+ return addresses;
+ }
+
+ /**
+ * Maps IP addresses to 2-letter country codes, or ?? if unknown.
+ *
+ * @param {string} ip The IP address to look for
+ * @returns {Promise<string>} A promise with the country code. If unknown, the
+ * promise is resolved with "??". It is rejected only when the underlying
+ * GETINFO command fails or if an exception is thrown
+ */
+ async getIPCountry(ip) {
+ this.#expectString(ip, "ip");
+ return this.#getInfo(`ip-to-country/${ip}`);
+ }
+
+ /**
+ * Ask tor which ports it is listening to for SOCKS connections.
+ *
+ * @returns {Promise<SocksListener[]>} An array of addresses. It might be
+ * empty (e.g., when DisableNetwork is set)
+ */
+ async getSocksListeners() {
+ const listeners = await this.#getInfo("net/listeners/socks");
+ return Array.from(
+ listeners.matchAll(/\s*("(?:[^"\\]|\\.)*"|\S+)\s*/g),
+ m => {
+ const listener = TorParsers.unescapeString(m[1]);
+ if (listener.startsWith("unix:/")) {
+ return { ipcPath: listener.substring(5) };
+ }
+ const idx = listener.lastIndexOf(":");
+ const host = listener.substring(0, idx);
+ const port = parseInt(listener.substring(idx + 1));
+ if (isNaN(port) || port <= 0 || port > 65535 || !host || !port) {
+ throw new Error(`Could not parse the SOCKS listener ${listener}.`);
+ }
+ return { host, port };
+ }
+ );
+ }
+
+ /**
+ * Ask Tor a list of circuits.
+ *
+ * @returns {CircuitInfo[]} An array with a string for each line
+ */
+ async getCircuits() {
+ const circuits = await this.#getInfo("circuit-status");
+ return circuits
+ .split(/\r?\n/)
+ .map(this.#parseCircBuilt.bind(this))
+ .filter(circ => circ);
+ }
+
+ // Configuration
+
+ /**
+ * Sends a GETCONF for a single key.
+ * The function could be easily generalized to get multiple keys at once, but
+ * we do not need this functionality, at the moment.
+ *
+ * @param {string} key The keys to get info for
+ * @returns {Promise<string[]>} The values obtained from the control port.
+ * The key is removed, and the values unescaped, but they are not parsed.
+ * The array might contain an empty string, which means that the default value
+ * is used
+ */
+ async #getConf(key) {
+ this.#expectString(key, "key");
+ // GETCONF expects a `keyword`, which should be only alpha characters,
+ // according to the definition in control-port.txt. But as a matter of fact,
+ // several configuration keys include numbers (e.g., Socks4Proxy). So, we
+ // accept also numbers in this regular expression. One of the reason to
+ // sanitize the input is that we then use it to create a regular expression.
+ // Sadly, JavaScript does not provide a function to escape/quote a string
+ // for inclusion in a regex. Should we remove this limitation, we should
+ // also implement a regex sanitizer, or switch to another pattern, like
+ // `([^=])` and then filter on the keyword.
+ if (!/^[A-Za-z0-9]+$/.test(key)) {
+ throw new Error("The key can be composed only of letters and numbers.");
+ }
+ const cmd = `GETCONF ${key}`;
+ const reply = await this.#sendCommand(cmd);
+ // From control-spec.txt: a 'default' value semantically different from an
+ // empty string will not have an equal sign, just `250 $key`.
+ const defaultRe = new RegExp(`^250[-\\s]${key}$`, "gim");
+ if (reply.match(defaultRe)) {
+ return [];
+ }
+ const re = new RegExp(`^250[-\\s]${key}=(.*)$`, "gim");
+ const values = Array.from(reply.matchAll(re), m =>
+ TorParsers.unescapeString(m[1])
+ );
+ if (!values.length) {
+ throw new TorProtocolError(cmd, reply);
+ }
+ return values;
+ }
+
+ /**
+ * Get the bridges Tor has been configured with.
+ *
+ * @returns {Bridge[]} The configured bridges
+ */
+ async getBridges() {
+ let missingId = false;
+ const bridges = (await this.#getConf("BRIDGE")).map(line => {
+ const info = TorParsers.parseBridgeLine(line);
+ if (!info.id) {
+ missingId = true;
+ }
+ return info;
+ });
+
+ // tor-browser#42541: bridge lines are allowed not to have a fingerprint.
+ // If such a bridge is in use, we will fail to associate it to the circuits,
+ // and the circuit display will not be shown.
+ // Tor provides a couple of GETINFO commands we can try to use to get more
+ // data about bridges, in particular GETINFO ns/purpose/bridge.
+ // While it tells us the bridge's IP (as configured by the user, which might
+ // be different from the real one with some PTs such as Snowflake), it does
+ // not tell the pluggable transport.
+ // Therefore, we need to start from the configured bridge lines, and if we
+ // detect that a bridge does not have a fingerprint, we try to associate one
+ // through its IP address and port.
+ // However, users can set them directly, therefore we might end up setting
+ // a fingerprint to the wrong line (e.g., if the IP address is reused).
+ // Also, we are not sure about when the data of ns/purpose/bridge is
+ // populated.
+ // Usually, we are interested only in the data of currently active bridges
+ // for the circuit display. So, as a matter of fact, we expect to have
+ // entries and to expose only the correct and working data in the frontend.
+ if (missingId) {
+ // See https://spec.torproject.org/dir-spec/consensus-formats.html.
+ // r <name> <identity> <digest> <date> <time> <address> <orport> <dirport>
+ const info = (await this.#getInfo("ns/purpose/bridge")).matchAll(
+ /^r\s+\S+\s+(?<identity>\S+)\s+\S+\s+\S+\s+\S+\s+(?<address>\S+)\s+(?<orport>\d+)/gm
+ );
+ const b64ToHex = b64 => {
+ let hex = "";
+ const raw = atob(b64);
+ for (let i = 0; i < raw.length; i++) {
+ hex += raw.charCodeAt(i).toString(16).toUpperCase().padStart(2, "0");
+ }
+ return hex;
+ };
+ const knownBridges = new Map(
+ Array.from(info, m => [
+ `${m.groups.address}:${m.groups.orport}`,
+ b64ToHex(m.groups.identity),
+ ])
+ );
+ for (const b of bridges) {
+ if (!b.id) {
+ // We expect the addresses of these lines to be only IPv4, therefore
+ // we do not check for brackets, even though they might be matched by
+ // our regex.
+ b.id = knownBridges.get(b.addr) ?? "";
+ }
+ }
+ }
+
+ return bridges;
+ }
+
+ /**
+ * Get the configured pluggable transports.
+ *
+ * @returns {PTInfo[]} An array with the info of all the configured pluggable
+ * transports.
+ */
+ async getPluggableTransports() {
+ return (await this.#getConf("ClientTransportPlugin")).map(ptLine => {
+ // man 1 tor: ClientTransportPlugin transport socks4|socks5 IP:PORT
+ const socksLine = ptLine.match(
+ /(\S+)\s+(socks[45])\s+([\d.]{7,15}|\[[\da-fA-F:]+\]):(\d{1,5})/i
+ );
+ // man 1 tor: transport exec path-to-binary [options]
+ const execLine = ptLine.match(
+ /(\S+)\s+(exec)\s+("(?:[^"\\]|\\.)*"|\S+)\s*(.*)/i
+ );
+ if (socksLine) {
+ return {
+ transports: socksLine[1].split(","),
+ type: socksLine[2].toLowerCase(),
+ ip: socksLine[3],
+ port: parseInt(socksLine[4], 10),
+ };
+ } else if (execLine) {
+ return {
+ transports: execLine[1].split(","),
+ type: execLine[2].toLowerCase(),
+ pathToBinary: TorParsers.unescapeString(execLine[3]),
+ options: execLine[4],
+ };
+ }
+ throw new Error(
+ `Received an invalid ClientTransportPlugin line: ${ptLine}`
+ );
+ });
+ }
+
+ /**
+ * Send multiple configuration values to tor.
+ *
+ * @param {Array} values The values to set. It should be an array of
+ * [key, value] pairs to pass to SETCONF. Keys can be repeated, and array
+ * values will be automatically unrolled.
+ */
+ async setConf(values) {
+ // NOTE: This is an async method. It must ensure that sequential calls to
+ // this method do not race against each other. I.e. the last call to this
+ // method must always be the last in #commandQueue. Otherwise a delayed
+ // earlier call could overwrite the configuration of a later call.
+ const args = values
+ .flatMap(([key, value]) => {
+ if (value === undefined || value === null) {
+ return [key];
+ }
+ if (Array.isArray(value)) {
+ return value.length
+ ? value.map(v => `${key}=${TorParsers.escapeString(v)}`)
+ : key;
+ } else if (typeof value === "string" || value instanceof String) {
+ return `${key}=${TorParsers.escapeString(value)}`;
+ } else if (typeof value === "boolean") {
+ return `${key}=${value ? "1" : "0"}`;
+ } else if (typeof value === "number") {
+ return `${key}=${value}`;
+ }
+ throw new Error(`Unsupported type ${typeof value} (key ${key})`);
+ })
+ .join(" ");
+ await this.#sendCommandSimple(`SETCONF ${args}`);
+ }
+
+ /**
+ * Enable or disable the network.
+ * Notice: switching from network disabled to network enabled will trigger a
+ * bootstrap on C tor! (Or stop the current one).
+ *
+ * @param {boolean} enabled Tell whether the network should be enabled
+ */
+ async setNetworkEnabled(enabled) {
+ await this.setConf([["DisableNetwork", !enabled]]);
+ }
+
+ /**
+ * Ask Tor to write out its config options into its torrc.
+ */
+ async flushSettings() {
+ await this.#sendCommandSimple("SAVECONF");
+ }
+
+ // Onion service authentication
+
+ /**
+ * Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private
+ * keys.
+ *
+ * @returns {OnionAuthKeyInfo[]}
+ */
+ async onionAuthViewKeys() {
+ const cmd = "onion_client_auth_view";
+ const message = await this.#sendCommand(cmd);
+ // Either `250-CLIENT`, or `250 OK` if no keys are available.
+ if (!message.startsWith("250")) {
+ throw new TorProtocolError(cmd, message);
+ }
+ const re =
+ /^250-CLIENT\s+(?<HSAddress>[A-Za-z2-7]+)\s+(?<KeyType>[^:]+):(?<PrivateKeyBlob>\S+)(?:\s(?<other>.+))?$/gim;
+ return Array.from(message.matchAll(re), match => {
+ // TODO: Change the consumer and make the fields more consistent with what
+ // we get (e.g., separate key and type, and use a boolen for permanent).
+ const info = {
+ address: match.groups.HSAddress,
+ keyType: match.groups.KeyType,
+ keyBlob: match.groups.PrivateKeyBlob,
+ flags: [],
+ };
+ const maybeFlags = match.groups.other?.match(/Flags=(\S+)/);
+ if (maybeFlags) {
+ info.flags = maybeFlags[1].split(",");
+ }
+ return info;
+ });
+ }
+
+ /**
+ * Sends an ONION_CLIENT_AUTH_ADD command to add a private key to the Tor
+ * configuration.
+ *
+ * @param {string} address The address of the onion service
+ * @param {string} b64PrivateKey The private key of the service, in base64
+ * @param {boolean} isPermanent Tell whether the key should be saved forever
+ */
+ async onionAuthAdd(address, b64PrivateKey, isPermanent) {
+ this.#expectString(address, "address");
+ this.#expectString(b64PrivateKey, "b64PrivateKey");
+ const keyType = "x25519";
+ let cmd = `onion_client_auth_add ${address} ${keyType}:${b64PrivateKey}`;
+ if (isPermanent) {
+ cmd += " Flags=Permanent";
+ }
+ const reply = await this.#sendCommand(cmd);
+ const status = reply.substring(0, 3);
+ if (status !== "250" && status !== "251" && status !== "252") {
+ throw new TorProtocolError(cmd, reply);
+ }
+ }
+
+ /**
+ * Sends an ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
+ * Tor configuration.
+ *
+ * @param {string} address The address of the onion service
+ */
+ async onionAuthRemove(address) {
+ this.#expectString(address, "address");
+ const cmd = `onion_client_auth_remove ${address}`;
+ const reply = await this.#sendCommand(cmd);
+ const status = reply.substring(0, 3);
+ if (status !== "250" && status !== "251") {
+ throw new TorProtocolError(cmd, reply);
+ }
+ }
+
+ // Daemon ownership
+
+ /**
+ * Instructs Tor to shut down when this control connection is closed.
+ * If multiple connection sends this request, Tor will shut dwon when any of
+ * them is closed.
+ */
+ async takeOwnership() {
+ await this.#sendCommandSimple("TAKEOWNERSHIP");
+ }
+
+ /**
+ * The __OwningControllerProcess argument can be used to make Tor periodically
+ * check if a certain PID is still present, or terminate itself otherwise.
+ * When switching to the ownership tied to the control port, this mechanism
+ * should be stopped by calling this function.
+ */
+ async resetOwningControllerProcess() {
+ await this.#sendCommandSimple("RESETCONF __OwningControllerProcess");
+ }
+
+ // Signals
+
+ /**
+ * Ask Tor to swtich to new circuits and clear the DNS cache.
+ */
+ async newnym() {
+ await this.#sendCommandSimple("SIGNAL NEWNYM");
+ }
+
+ // Events monitoring
+
+ /**
+ * Enable receiving certain events.
+ * As per control-spec.txt, any events turned on in previous calls but not
+ * included in this one will be turned off.
+ *
+ * @param {string[]} types The events to enable. If empty, no events will be
+ * watched.
+ * @returns {Promise<void>}
+ */
+ setEvents(types) {
+ if (!types.every(t => typeof t === "string" || t instanceof String)) {
+ throw new Error("Event types must be strings");
+ }
+ return this.#sendCommandSimple("SETEVENTS " + types.join(" "));
+ }
+
+ /**
+ * Parse an asynchronous event and pass the data to the relative handler.
+ * Only single-line messages are currently supported.
+ *
+ * @param {string} message The message received on the control port. It should
+ * starts with `"650" SP`.
+ */
+ #handleNotification(message) {
+ if (!this.#eventHandler) {
+ return;
+ }
+ const data = message.match(/^650\s+(?<type>\S+)\s*(?<data>.*)?/);
+ if (!data) {
+ return;
+ }
+ switch (data.groups.type) {
+ case "STATUS_CLIENT": {
+ let status;
+ try {
+ status = this.#parseBootstrapStatus(data.groups.data);
+ } catch (e) {
+ // Probably, a non bootstrap client status
+ logger.debug(`Failed to parse STATUS_CLIENT: ${data.groups.data}`, e);
+ break;
+ }
+ this.#eventHandler.onBootstrapStatus(status);
+ break;
+ }
+ case "CIRC": {
+ const maybeCircuit = this.#parseCircBuilt(data.groups.data);
+ const closedEvent = /^(?<ID>[a-zA-Z0-9]{1,16})\sCLOSED/.exec(
+ data.groups.data
+ );
+ if (maybeCircuit) {
+ this.#eventHandler.onCircuitBuilt(
+ maybeCircuit.id,
+ maybeCircuit.nodes
+ );
+ } else if (closedEvent) {
+ this.#eventHandler.onCircuitClosed(closedEvent.groups.ID);
+ }
+ break;
+ }
+ case "STREAM": {
+ const sentConnectEvent =
+ /^(?<StreamID>[a-zA-Z0-9]{1,16})\sSENTCONNECT\s(?<CircuitID>[a-zA-Z0-9]{1,16})/.exec(
+ data.groups.data
+ );
+ if (sentConnectEvent) {
+ const credentials = this.#parseCredentials(data.groups.data);
+ this.#eventHandler.onStreamSentConnect(
+ sentConnectEvent.groups.StreamID,
+ sentConnectEvent.groups.CircuitID,
+ credentials?.username ?? null,
+ credentials?.password ?? null
+ );
+ }
+ break;
+ }
+ case "NOTICE":
+ case "WARN":
+ case "ERR":
+ this.#eventHandler.onLogMessage(data.groups.type, data.groups.data);
+ break;
+ }
+ }
+
+ // Parsers
+
+ /**
+ * Parse a bootstrap status line.
+ *
+ * @param {string} line The line to parse, without the command/notification
+ * prefix
+ * @returns {object} An object with the bootstrap information received from
+ * Tor. Its keys might vary, depending on the input
+ */
+ #parseBootstrapStatus(line) {
+ const match = line.match(/^(NOTICE|WARN) BOOTSTRAP\s*(.*)/);
+ if (!match) {
+ throw Error(
+ `Received an invalid response for the bootstrap phase: ${line}`
+ );
+ }
+ const status = {
+ // Type is actually StatusSeverity in the specifications.
+ TYPE: match[1],
+ ...this.#getKeyValues(match[2]),
+ };
+ if (status.PROGRESS !== undefined) {
+ status.PROGRESS = parseInt(status.PROGRESS, 10);
+ }
+ if (status.COUNT !== undefined) {
+ status.COUNT = parseInt(status.COUNT, 10);
+ }
+ return status;
+ }
+
+ /**
+ * Parse a CIRC BUILT event or a GETINFO circuit-status.
+ *
+ * @param {string} line The line to parse
+ * @returns {CircuitInfo?} The ID and nodes of the circuit, or null if the
+ * parsing failed.
+ */
+ #parseCircBuilt(line) {
+ const builtEvent =
+ /^(?<ID>[a-zA-Z0-9]{1,16})\sBUILT\s(?<Path>(,?\$([0-9a-fA-F]{40})(?:~[a-zA-Z0-9]{1,19})?)+)/.exec(
+ line
+ );
+ if (!builtEvent) {
+ return null;
+ }
+ const fp = /\$([0-9a-fA-F]{40})/g;
+ const nodes = Array.from(builtEvent.groups.Path.matchAll(fp), g =>
+ g[1].toUpperCase()
+ );
+ // 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.
+ // So we do not try to extract them for now. Otherwise, we could do
+ // const credentials = this.#parseCredentials(line);
+ return { id: builtEvent.groups.ID, nodes };
+ }
+
+ /**
+ * Check if a STREAM or CIRC response line contains SOCKS_USERNAME and
+ * SOCKS_PASSWORD.
+ *
+ * @param {string} line The circ or stream line to check
+ * @returns {object?} The credentials, or null if not found
+ */
+ #parseCredentials(line) {
+ const username = /SOCKS_USERNAME=("(?:[^"\\]|\\.)*")/.exec(line);
+ const password = /SOCKS_PASSWORD=("(?:[^"\\]|\\.)*")/.exec(line);
+ return username && password
+ ? {
+ username: TorParsers.unescapeString(username[1]),
+ password: TorParsers.unescapeString(password[1]),
+ }
+ : null;
+ }
+
+ /**
+ * Return an object with all the matches that are in the form `key="value"` or
+ * `key=value`. The values will be unescaped, but no additional parsing will
+ * be done (e.g., numbers will be returned as strings).
+ * If keys are repeated, only the last one will be taken.
+ *
+ * @param {string} str The string to match tokens in
+ * @returns {object} An object with all the various tokens. If none is found,
+ * an empty object is returned.
+ */
+ #getKeyValues(str) {
+ return Object.fromEntries(
+ Array.from(
+ str.matchAll(/\s*([^=]+)=("(?:[^"\\]|\\.)*"|\S+)\s*/g) || [],
+ pair => [pair[1], TorParsers.unescapeString(pair[2])]
+ )
+ );
+ }
+
+ // Other helpers
+
+ /**
+ * Throw an exception when value is not a string.
+ *
+ * @param {any} value The value to check
+ * @param {string} name The name of the `value` argument
+ */
+ #expectString(value, name) {
+ if (typeof value !== "string" && !(value instanceof String)) {
+ throw new Error(`The ${name} argument is expected to be a string.`);
+ }
+ }
+}
+
+/**
+ * @typedef {object} TorEventHandler
+ * The event handler interface.
+ * The controller owner can implement this methods to receive asynchronous
+ * notifications from the controller.
+ *
+ * @property {OnBootstrapStatus} onBootstrapStatus Called when a bootstrap
+ * status is received (i.e., a STATUS_CLIENT event with a BOOTSTRAP action)
+ * @property {OnLogMessage} onLogMessage Called when a log message is received
+ * (i.e., a NOTICE, WARN or ERR notification)
+ * @property {OnCircuitBuilt} onCircuitBuilt Called when a circuit is built
+ * (i.e., a CIRC event with a BUILT status)
+ * @property {OnCircuitClosed} onCircuitClosed Called when a circuit is closed
+ * (i.e., a CIRC event with a CLOSED status)
+ * @property {OnStreamSentConnect} onStreamSentConnect Called when a stream sent
+ * a connect cell along a circuit (i.e., a STREAM event with a SENTCONNECT
+ * status)
+ */
+/**
+ * @callback OnBootstrapStatus
+ *
+ * @param {object} status An object with the bootstrap information. Its keys
+ * depend on what the arguments sent by the tor daemon
+ */
+/**
+ * @callback OnLogMessage
+ *
+ * @param {string} type The type of message (NOTICE, WARNING, ERR, etc...)
+ * @param {string} message The actual log message
+ */
+/**
+ * @callback OnCircuitBuilt
+ *
+ * @param {CircuitID} id The id of the circuit that has been built
+ * @param {NodeFingerprint[]} nodes The onion routers composing the circuit
+ */
+/**
+ * @callback OnCircuitClosed
+ *
+ * @param {CircuitID} id The id of the circuit that has been closed
+ */
+/**
+ * @callback OnStreamSentConnect
+ *
+ * @param {StreamID} streamId The id of the stream that switched to the succeeded
+ * state
+ * @param {CircuitID} circuitId The id of the circuit the stream is using
+ * @param {string?} username The SOCKS username associated to the stream, or
+ * null if not available
+ * @param {string?} username The SOCKS password associated to the stream, or
+ * null if not available
+ */
diff --git a/toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs b/toolkit/components/tor-launcher/TorLauncherUtil.sys.mjs
@@ -0,0 +1,702 @@
+// Copyright (c) 2022, The Tor Project, Inc.
+// See LICENSE for licensing information.
+
+/*************************************************************************
+ * Tor Launcher Util JS Module
+ *************************************************************************/
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+});
+
+const kPropBundleURI = "chrome://torbutton/locale/torlauncher.properties";
+const kPropNamePrefix = "torlauncher.";
+const kIPCDirPrefName = "extensions.torlauncher.tmp_ipc_dir";
+
+/**
+ * This class allows to lookup for the paths of the various files that are
+ * needed or can be used with the tor daemon, such as its configuration, the
+ * GeoIP databases, and the Unix sockets that can be optionally used for the
+ * control and the SOCKS ports.
+ */
+class TorFile {
+ // The nsIFile to be returned
+ file = null;
+
+ isIPC = false;
+ ipcFileName = "";
+ checkIPCPathLen = true;
+
+ static _isFirstIPCPathRequest = true;
+ static _dataDir = null;
+ static _appDir = null;
+ static _torDir = null;
+
+ constructor(aTorFileType, aCreate) {
+ this.fileType = aTorFileType;
+
+ this.getFromPref();
+ this.getIPC();
+ // No preference and no pre-determined IPC path: use a default path.
+ if (!this.file) {
+ this.getDefault();
+ }
+ // At this point, this.file must not be null, or previous functions must
+ // have thrown and interrupted this constructor.
+ if (!this.file.exists() && !this.isIPC && aCreate) {
+ this.createFile();
+ }
+ this.normalize();
+ }
+
+ getFile() {
+ return this.file;
+ }
+
+ getFromPref() {
+ const prefName = `extensions.torlauncher.${this.fileType}_path`;
+ const path = Services.prefs.getCharPref(prefName, "");
+ if (path) {
+ const isUserData =
+ this.fileType !== "tor" &&
+ this.fileType !== "pt-startup-dir" &&
+ this.fileType !== "torrc-defaults";
+ // always try to use path if provided in pref
+ this.checkIPCPathLen = false;
+ this.setFileFromPath(path, isUserData);
+ }
+ }
+
+ getIPC() {
+ const isControlIPC = this.fileType === "control_ipc";
+ const isSOCKSIPC = this.fileType === "socks_ipc";
+ this.isIPC = isControlIPC || isSOCKSIPC;
+ if (!this.isIPC) {
+ return;
+ }
+
+ const kControlIPCFileName = "control.socket";
+ const kSOCKSIPCFileName = "socks.socket";
+ this.ipcFileName = isControlIPC ? kControlIPCFileName : kSOCKSIPCFileName;
+ this.extraIPCPathLen = this.isSOCKSIPC ? 2 : 0;
+
+ // Do not do anything else if this.file has already been populated with the
+ // _path preference for this file type (or if we are not looking for an IPC
+ // file).
+ if (this.file) {
+ return;
+ }
+
+ // If this is the first request for an IPC path during this browser
+ // session, remove the old temporary directory. This helps to keep /tmp
+ // clean if the browser crashes or is killed.
+ if (TorFile._isFirstIPCPathRequest) {
+ TorLauncherUtil.cleanupTempDirectories();
+ TorFile._isFirstIPCPathRequest = false;
+ } else {
+ // FIXME: Do we really need a preference? Or can we save it in a static
+ // member?
+ // Retrieve path for IPC objects (it may have already been determined).
+ const ipcDirPath = Services.prefs.getCharPref(kIPCDirPrefName, "");
+ if (ipcDirPath) {
+ // We have already determined where IPC objects will be placed.
+ this.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ this.file.initWithPath(ipcDirPath);
+ this.file.append(this.ipcFileName);
+ this.checkIPCPathLen = false; // already checked.
+ return;
+ }
+ }
+
+ // If XDG_RUNTIME_DIR is set, use it as the base directory for IPC
+ // objects (e.g., Unix domain sockets) -- assuming it is not too long.
+ if (!Services.env.exists("XDG_RUNTIME_DIR")) {
+ return;
+ }
+ const ipcDir = this.createUniqueIPCDir(Services.env.get("XDG_RUNTIME_DIR"));
+ if (ipcDir) {
+ const f = ipcDir.clone();
+ f.append(this.ipcFileName);
+ if (this.isIPCPathLengthOK(f.path, this.extraIPCPathLen)) {
+ this.file = f;
+ this.checkIPCPathLen = false; // no need to check again.
+
+ // Store directory path so it can be reused for other IPC objects
+ // and so it can be removed during exit.
+ Services.prefs.setCharPref(kIPCDirPrefName, ipcDir.path);
+ } else {
+ // too long; remove the directory that we just created.
+ ipcDir.remove(false);
+ }
+ }
+ }
+
+ getDefault() {
+ switch (this.fileType) {
+ case "tor":
+ this.file = TorFile.torDir;
+ this.file.append(TorLauncherUtil.isWindows ? "tor.exe" : "tor");
+ break;
+ case "torrc-defaults":
+ if (TorLauncherUtil.isMac) {
+ this.file = TorFile.appDir;
+ this.file.appendRelativePath(
+ "Contents/Resources/TorBrowser/Tor/torrc-defaults"
+ );
+ } else {
+ // FIXME: Should we move this file to the tor directory, in the other
+ // platforms, since it is not user data?
+ this.file = TorFile.torDataDir;
+ this.file.append("torrc-defaults");
+ }
+ break;
+ case "torrc":
+ this.file = TorFile.torDataDir;
+ this.file.append("torrc");
+ break;
+ case "tordatadir":
+ this.file = TorFile.torDataDir;
+ break;
+ case "toronionauthdir":
+ this.file = TorFile.torDataDir;
+ this.file.append("onion-auth");
+ break;
+ case "pt-startup-dir":
+ // On macOS we specify different relative paths than on Linux and
+ // Windows
+ this.file = TorLauncherUtil.isMac ? TorFile.torDir : TorFile.appDir;
+ break;
+ default:
+ if (!TorLauncherUtil.isWindows && this.isIPC) {
+ this.setFileFromPath(`Tor/${this.ipcFileName}`, true);
+ break;
+ }
+ throw new Error("Unknown file type");
+ }
+ }
+
+ // This function is used to set this.file from a string that contains a path.
+ // As a matter of fact, it is used only when setting a path from preferences,
+ // or to set the default IPC paths.
+ setFileFromPath(path, isUserData) {
+ if (TorLauncherUtil.isWindows) {
+ path = path.replaceAll("/", "\\");
+ }
+ // Turn 'path' into an absolute path when needed.
+ if (TorLauncherUtil.isPathRelative(path)) {
+ if (TorLauncherUtil.isMac) {
+ // On macOS, files are correctly separated because it was needed for the
+ // gatekeeper signing.
+ this.file = isUserData ? TorFile.dataDir : TorFile.appDir;
+ } else {
+ // Windows and Linux still use the legacy behavior.
+ // To avoid breaking old installations, let's just keep it.
+ this.file = TorFile.appDir;
+ this.file.append("TorBrowser");
+ }
+ this.file.appendRelativePath(path);
+ } else {
+ this.file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ this.file.initWithPath(path);
+ }
+ }
+
+ createFile() {
+ if (
+ "tordatadir" == this.fileType ||
+ "toronionauthdir" == this.fileType ||
+ "pt-profiles-dir" == this.fileType
+ ) {
+ this.file.create(this.file.DIRECTORY_TYPE, 0o700);
+ } else {
+ this.file.create(this.file.NORMAL_FILE_TYPE, 0o600);
+ }
+ }
+
+ // If the file exists or an IPC object was requested, normalize the path
+ // and return a file object. The control and SOCKS IPC objects will be
+ // created by tor.
+ normalize() {
+ if (this.file.exists()) {
+ try {
+ this.file.normalize();
+ } catch (e) {
+ console.warn("Normalization of the path failed", e);
+ }
+ } else if (!this.isIPC) {
+ throw new Error(`${this.fileType} file not found: ${this.file.path}`);
+ }
+
+ // Ensure that the IPC path length is short enough for use by the
+ // operating system. If not, create and use a unique directory under
+ // /tmp for all IPC objects. The created directory path is stored in
+ // a preference so it can be reused for other IPC objects and so it
+ // can be removed during exit.
+ if (
+ this.isIPC &&
+ this.checkIPCPathLen &&
+ !this.isIPCPathLengthOK(this.file.path, this.extraIPCPathLen)
+ ) {
+ this.file = this.createUniqueIPCDir("/tmp");
+ if (!this.file) {
+ throw new Error("failed to create unique directory under /tmp");
+ }
+
+ Services.prefs.setCharPref(kIPCDirPrefName, this.file.path);
+ this.file.append(this.ipcFileName);
+ }
+ }
+
+ // Return true if aPath is short enough to be used as an IPC object path,
+ // e.g., for a Unix domain socket path. aExtraLen is the "delta" necessary
+ // to accommodate other IPC objects that have longer names; it is used to
+ // account for "control.socket" vs. "socks.socket" (we want to ensure that
+ // all IPC objects are placed in the same parent directory unless the user
+ // has set prefs or env vars to explicitly specify the path for an object).
+ // We enforce a maximum length of 100 because all operating systems allow
+ // at least 100 characters for Unix domain socket paths.
+ isIPCPathLengthOK(aPath, aExtraLen) {
+ const kMaxIPCPathLen = 100;
+ return aPath && aPath.length + aExtraLen <= kMaxIPCPathLen;
+ }
+
+ // Returns an nsIFile or null if a unique directory could not be created.
+ createUniqueIPCDir(aBasePath) {
+ try {
+ const d = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ d.initWithPath(aBasePath);
+ d.append("Tor");
+ d.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o700);
+ return d;
+ } catch (e) {
+ console.error(`createUniqueIPCDir failed for ${aBasePath}: `, e);
+ return null;
+ }
+ }
+
+ // Returns an nsIFile that points to the binary directory (on Linux and
+ // Windows), and to the root of the application bundle on macOS.
+ static get appDir() {
+ if (!this._appDir) {
+ // .../Browser on Windows and Linux, .../TorBrowser.app/Contents/MacOS/ on
+ // macOS.
+ this._appDir = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent;
+ if (TorLauncherUtil.isMac) {
+ this._appDir = this._appDir.parent.parent;
+ }
+ }
+ return this._appDir.clone();
+ }
+
+ // Returns an nsIFile that points to the data directory. This is usually
+ // TorBrowser/Data/ on Linux and Windows, and TorBrowser-Data/ on macOS.
+ // The parent directory of the default profile directory is taken.
+ static get dataDir() {
+ if (!this._dataDir) {
+ // Notice that we use `DefProfRt`, because users could create their
+ // profile in a completely unexpected directory: the profiles.ini contains
+ // a IsRelative entry, which I expect could influence ProfD, but not this.
+ this._dataDir = Services.dirsvc.get("DefProfRt", Ci.nsIFile).parent;
+ }
+ return this._dataDir.clone();
+ }
+
+ // Returns an nsIFile that points to the directory that contains the tor
+ // executable.
+ static get torDir() {
+ if (!this._torDir) {
+ // The directory that contains firefox
+ const torDir = Services.dirsvc.get("XREExeF", Ci.nsIFile).parent;
+ if (!TorLauncherUtil.isMac) {
+ torDir.append("TorBrowser");
+ }
+ torDir.append("Tor");
+ // Save the value only if the XPCOM methods do not throw.
+ this._torDir = torDir;
+ }
+ return this._torDir.clone();
+ }
+
+ // Returns an nsIFile that points to the directory that contains the tor
+ // data. Currently it is ${dataDir}/Tor.
+ static get torDataDir() {
+ const dir = this.dataDir;
+ dir.append("Tor");
+ return dir;
+ }
+}
+
+export const TorLauncherUtil = {
+ get isAndroid() {
+ return Services.appinfo.OS === "Android";
+ },
+
+ get isLinux() {
+ // Use AppConstants for Linux rather then appinfo because we are sure it
+ // will catch also various Unix flavors for which unofficial ports might
+ // exist (which should work as Linux, as far as we know).
+ return AppConstants.platform === "linux";
+ },
+
+ get isMac() {
+ return Services.appinfo.OS === "Darwin";
+ },
+
+ get isWindows() {
+ return Services.appinfo.OS === "WINNT";
+ },
+
+ isPathRelative(path) {
+ const re = this.isWindows ? /^([A-Za-z]:|\\)\\/ : /^\//;
+ return !re.test(path);
+ },
+
+ // Returns true if user confirms; false if not.
+ showConfirm(aParentWindow, aMsg, aDefaultButtonLabel, aCancelButtonLabel) {
+ if (!aParentWindow) {
+ aParentWindow = Services.wm.getMostRecentWindow("navigator:browser");
+ }
+
+ const ps = Services.prompt;
+ const title = this.getLocalizedString("error_title");
+ const btnFlags =
+ ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING +
+ ps.BUTTON_POS_0_DEFAULT +
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_IS_STRING;
+
+ const notUsed = { value: false };
+ const btnIndex = ps.confirmEx(
+ aParentWindow,
+ title,
+ aMsg,
+ btnFlags,
+ aDefaultButtonLabel,
+ aCancelButtonLabel,
+ null,
+ null,
+ notUsed
+ );
+ return btnIndex === 0;
+ },
+
+ /**
+ * Ask the user whether they desire to restart tor.
+ *
+ * @param {boolean} initError If we could connect to the control port at
+ * least once and we are showing this prompt because the tor process exited
+ * suddenly, we will display a different message
+ * @returns {boolean} true if the user asked to restart tor
+ */
+ showRestartPrompt(initError) {
+ let s;
+ if (initError) {
+ const key = "tor_exited_during_startup";
+ s = this.getLocalizedString(key);
+ } else {
+ // tor exited suddenly, so configuration should be okay
+ s =
+ this.getLocalizedString("tor_exited") +
+ "\n\n" +
+ this.getLocalizedString("tor_exited2");
+ }
+ const defaultBtnLabel = this.getLocalizedString("restart_tor");
+ let cancelBtnLabel = "OK";
+ try {
+ const kSysBundleURI = "chrome://global/locale/commonDialogs.properties";
+ const sysBundle = Services.strings.createBundle(kSysBundleURI);
+ cancelBtnLabel = sysBundle.GetStringFromName(cancelBtnLabel);
+ } catch (e) {
+ console.warn("Could not localize the cancel button", e);
+ }
+ return this.showConfirm(null, s, defaultBtnLabel, cancelBtnLabel);
+ },
+
+ _stringBundle: null,
+
+ // Localized Strings
+ // TODO: Switch to fluent also these ones.
+
+ // "torlauncher." is prepended to aStringName.
+ getLocalizedString(aStringName) {
+ if (!aStringName) {
+ return aStringName;
+ }
+ if (!this._stringBundle) {
+ this._stringBundle = Services.strings.createBundle(kPropBundleURI);
+ }
+ try {
+ const key = kPropNamePrefix + aStringName;
+ return this._stringBundle.GetStringFromName(key);
+ } catch (e) {}
+ return aStringName;
+ },
+
+ /**
+ * Determine what kind of SOCKS port has been requested for this session or
+ * the browser has been configured for.
+ * On Windows (where Unix domain sockets are not supported), TCP is always
+ * used.
+ *
+ * The following environment variables are supported and take precedence over
+ * preferences:
+ * TOR_TRANSPROXY (do not use a proxy)
+ * TOR_SOCKS_IPC_PATH (file system path; ignored on Windows)
+ * TOR_SOCKS_HOST
+ * TOR_SOCKS_PORT
+ *
+ * The following preferences are consulted:
+ * network.proxy.socks
+ * network.proxy.socks_port
+ * extensions.torlauncher.socks_port_use_ipc (Boolean)
+ * extensions.torlauncher.socks_ipc_path (file system path)
+ * If extensions.torlauncher.socks_ipc_path is empty, a default path is used.
+ *
+ * When using TCP, if a value is not defined via an env variable it is
+ * taken from the corresponding browser preference if possible. The
+ * exceptions are:
+ * If network.proxy.socks contains a file: URL, a default value of
+ * "127.0.0.1" is used instead.
+ * If the network.proxy.socks_port value is not valid (outside the
+ * (0; 65535] range), we will let the tor daemon choose a port.
+ *
+ * The SOCKS configuration will not influence the launch of a tor daemon and
+ * the configuration of the control port in any way.
+ * When a SOCKS configuration is required without TOR_SKIP_LAUNCH, the browser
+ * will try to configure the tor instance to use the required configuration.
+ * This also applies to TOR_TRANSPROXY (at least for now): tor will be
+ * launched with its defaults.
+ *
+ * @returns {SocksSettings}
+ */
+ getPreferredSocksConfiguration() {
+ if (Services.env.exists("TOR_TRANSPROXY")) {
+ return { transproxy: true };
+ }
+
+ let useIPC;
+ const socksPortInfo = {
+ transproxy: false,
+ };
+
+ if (!this.isWindows && Services.env.exists("TOR_SOCKS_IPC_PATH")) {
+ useIPC = true;
+ const ipcPath = Services.env.get("TOR_SOCKS_IPC_PATH");
+ if (ipcPath) {
+ socksPortInfo.ipcFile = new lazy.FileUtils.File(ipcPath);
+ }
+ } else {
+ // Check for TCP host and port environment variables.
+ if (Services.env.exists("TOR_SOCKS_HOST")) {
+ socksPortInfo.host = Services.env.get("TOR_SOCKS_HOST");
+ useIPC = false;
+ }
+ if (Services.env.exists("TOR_SOCKS_PORT")) {
+ const port = parseInt(Services.env.get("TOR_SOCKS_PORT"), 10);
+ if (Number.isInteger(port) && port >= 0 && port <= 65535) {
+ socksPortInfo.port = port;
+ useIPC = false;
+ }
+ }
+ }
+
+ if (useIPC === undefined) {
+ useIPC =
+ !this.isWindows &&
+ Services.prefs.getBoolPref(
+ "extensions.torlauncher.socks_port_use_ipc",
+ false
+ );
+ }
+
+ // Fill in missing SOCKS info from prefs.
+ if (useIPC) {
+ if (!socksPortInfo.ipcFile) {
+ socksPortInfo.ipcFile = TorLauncherUtil.getTorFile("socks_ipc", false);
+ }
+ } else {
+ if (!socksPortInfo.host) {
+ let socksAddr = Services.prefs.getCharPref(
+ "network.proxy.socks",
+ "127.0.0.1"
+ );
+ let socksAddrHasHost = socksAddr && !socksAddr.startsWith("file:");
+ socksPortInfo.host = socksAddrHasHost ? socksAddr : "127.0.0.1";
+ }
+
+ if (socksPortInfo.port === undefined) {
+ let socksPort = Services.prefs.getIntPref(
+ "network.proxy.socks_port",
+ 9150
+ );
+ if (socksPort > 0 && socksPort <= 65535) {
+ socksPortInfo.port = socksPort;
+ } else {
+ // Automatic port number, we have to query tor over the control port
+ // every time we change DisableNetwork.
+ socksPortInfo.port = 0;
+ }
+ }
+ }
+
+ return socksPortInfo;
+ },
+
+ /**
+ * Apply our proxy configuration to the browser.
+ *
+ * Currently, we try to configure the Tor daemon to match the browser's
+ * configuration, but this might change in the future (tor-browser#42062).
+ *
+ * @param {SocksSettings} socksPortInfo The configuration to apply
+ */
+ setProxyConfiguration(socksPortInfo) {
+ if (socksPortInfo.transproxy) {
+ Services.prefs.setBoolPref("network.proxy.socks_remote_dns", false);
+ Services.prefs.setIntPref("network.proxy.type", 0);
+ Services.prefs.setIntPref("network.proxy.socks_port", 0);
+ Services.prefs.setCharPref("network.proxy.socks", "");
+ return;
+ }
+
+ if (socksPortInfo.ipcFile) {
+ const fph = Services.io
+ .getProtocolHandler("file")
+ .QueryInterface(Ci.nsIFileProtocolHandler);
+ const fileURI = fph.newFileURI(socksPortInfo.ipcFile);
+ Services.prefs.setCharPref("network.proxy.socks", fileURI.spec);
+ Services.prefs.setIntPref("network.proxy.socks_port", 0);
+ } else {
+ if (socksPortInfo.host) {
+ Services.prefs.setCharPref("network.proxy.socks", socksPortInfo.host);
+ }
+ if (socksPortInfo.port > 0 && socksPortInfo.port <= 65535) {
+ Services.prefs.setIntPref(
+ "network.proxy.socks_port",
+ socksPortInfo.port
+ );
+ }
+ }
+
+ if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) {
+ Services.prefs.setBoolPref("network.proxy.socks_remote_dns", true);
+ Services.prefs.setIntPref("network.proxy.type", 1);
+ }
+
+ // Force prefs to be synced to disk
+ Services.prefs.savePrefFile(null);
+ },
+
+ /**
+ * Determine the current value for whether we should start and own Tor.
+ *
+ * @returns {boolean} Whether we should start and own Tor.
+ */
+ _getShouldStartAndOwnTor() {
+ const kPrefStartTor = "extensions.torlauncher.start_tor";
+ try {
+ const kBrowserToolboxPort = "MOZ_BROWSER_TOOLBOX_PORT";
+ const kEnvSkipLaunch = "TOR_SKIP_LAUNCH";
+ const kEnvProvider = "TOR_PROVIDER";
+ if (Services.env.exists(kBrowserToolboxPort)) {
+ return false;
+ }
+ if (Services.env.exists(kEnvSkipLaunch)) {
+ const value = parseInt(Services.env.get(kEnvSkipLaunch));
+ return isNaN(value) || !value;
+ }
+ if (
+ Services.env.exists(kEnvProvider) &&
+ Services.env.get(kEnvProvider) === "none"
+ ) {
+ return false;
+ }
+ } catch (e) {}
+ return Services.prefs.getBoolPref(kPrefStartTor, true);
+ },
+
+ /**
+ * Cached value for shouldStartAndOwnTor, or `null` if not yet initialised.
+ *
+ * @type {boolean}
+ */
+ _shouldStartAndOwnTor: null,
+
+ /**
+ * Whether we should start and own Tor.
+ *
+ * The value should be constant per-session.
+ *
+ * @type {boolean}
+ */
+ get shouldStartAndOwnTor() {
+ // Do not want this value to change within the same session, so always used
+ // the cached valued if it is available.
+ if (this._shouldStartAndOwnTor === null) {
+ this._shouldStartAndOwnTor = this._getShouldStartAndOwnTor();
+ }
+ return this._shouldStartAndOwnTor;
+ },
+
+ get shouldShowNetworkSettings() {
+ try {
+ const kEnvForceShowNetConfig = "TOR_FORCE_NET_CONFIG";
+ if (Services.env.exists(kEnvForceShowNetConfig)) {
+ const value = parseInt(Services.env.get(kEnvForceShowNetConfig));
+ return !isNaN(value) && value;
+ }
+ } catch (e) {}
+ return true;
+ },
+
+ get shouldOnlyConfigureTor() {
+ const kPrefOnlyConfigureTor = "extensions.torlauncher.only_configure_tor";
+ try {
+ const kEnvOnlyConfigureTor = "TOR_CONFIGURE_ONLY";
+ if (Services.env.exists(kEnvOnlyConfigureTor)) {
+ const value = parseInt(Services.env.get(kEnvOnlyConfigureTor));
+ return !isNaN(value) && value;
+ }
+ } catch (e) {}
+ return Services.prefs.getBoolPref(kPrefOnlyConfigureTor, false);
+ },
+
+ // Returns an nsIFile.
+ // If aTorFileType is "control_ipc" or "socks_ipc", aCreate is ignored
+ // and there is no requirement that the IPC object exists.
+ // For all other file types, null is returned if the file does not exist
+ // and it cannot be created (it will be created if aCreate is true).
+ getTorFile(aTorFileType, aCreate) {
+ if (!aTorFileType) {
+ return null;
+ }
+ try {
+ const torFile = new TorFile(aTorFileType, aCreate);
+ return torFile.getFile();
+ } catch (e) {
+ console.error(`getTorFile: cannot get ${aTorFileType}`, e);
+ }
+ return null; // File not found or error (logged above).
+ },
+
+ cleanupTempDirectories() {
+ const dirPath = Services.prefs.getCharPref(kIPCDirPrefName, "");
+ try {
+ Services.prefs.clearUserPref(kIPCDirPrefName);
+ } catch (e) {}
+ try {
+ if (dirPath) {
+ const f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ f.initWithPath(dirPath);
+ if (f.exists()) {
+ f.remove(false);
+ }
+ }
+ } catch (e) {
+ console.warn("Could not remove the IPC directory", e);
+ }
+ },
+};
diff --git a/toolkit/components/tor-launcher/TorParsers.sys.mjs b/toolkit/components/tor-launcher/TorParsers.sys.mjs
@@ -0,0 +1,122 @@
+// Copyright (c) 2022, The Tor Project, Inc.
+
+export const TorStatuses = Object.freeze({
+ OK: 250,
+ EventNotification: 650,
+});
+
+export const TorParsers = Object.freeze({
+ // Escape non-ASCII characters for use within the Tor Control protocol.
+ // Based on Vidalia's src/common/stringutil.cpp:string_escape().
+ // Returns the new string.
+ escapeString(aStr) {
+ // Just return if all characters are printable ASCII excluding SP, ", and #
+ const kSafeCharRE = /^[\x21\x24-\x7E]*$/;
+ if (!aStr || kSafeCharRE.test(aStr)) {
+ return aStr;
+ }
+ const escaped = aStr
+ .replaceAll("\\", "\\\\")
+ .replaceAll('"', '\\"')
+ .replaceAll("\n", "\\n")
+ .replaceAll("\r", "\\r")
+ .replaceAll("\t", "\\t")
+ .replaceAll(/[^\x20-\x7e]+/g, text => {
+ const encoder = new TextEncoder();
+ return Array.from(
+ encoder.encode(text),
+ ch => "\\x" + ch.toString(16)
+ ).join("");
+ });
+ return `"${escaped}"`;
+ },
+
+ // Unescape Tor Control string aStr (removing surrounding "" and \ escapes).
+ // Based on Vidalia's src/common/stringutil.cpp:string_unescape().
+ // Returns the unescaped string. Throws upon failure.
+ // Within Torbutton, the file modules/utils.js also contains a copy of
+ // _strUnescape().
+ unescapeString(aStr) {
+ if (
+ !aStr ||
+ aStr.length < 2 ||
+ aStr[0] !== '"' ||
+ aStr[aStr.length - 1] !== '"'
+ ) {
+ return aStr;
+ }
+
+ // Regular expression by Tim Pietzcker
+ // https://stackoverflow.com/a/15569588
+ if (!/^(?:[^"\\]|\\.|"(?:\\.|[^"\\])*")*$/.test(aStr)) {
+ throw new Error('Unescaped " within string');
+ }
+
+ const matchUnicode = /^(\\x[0-9A-Fa-f]{2}|\\[0-7]{3})+/;
+ let rv = "";
+ let lastAdded = 1;
+ let bs;
+ while ((bs = aStr.indexOf("\\", lastAdded)) !== -1) {
+ rv += aStr.substring(lastAdded, bs);
+ // We always increment lastAdded, because we will either add something, or
+ // ignore the backslash.
+ lastAdded = bs + 2;
+ if (lastAdded === aStr.length) {
+ // The string ends with \", which is illegal
+ throw new Error("Missing character after \\");
+ }
+ switch (aStr[bs + 1]) {
+ case "n":
+ rv += "\n";
+ break;
+ case "r":
+ rv += "\r";
+ break;
+ case "t":
+ rv += "\t";
+ break;
+ case '"':
+ case "\\":
+ rv += aStr[bs + 1];
+ break;
+ default:
+ aStr.substring(bs).replace(matchUnicode, sequence => {
+ const bytes = [];
+ for (let i = 0; i < sequence.length; i += 4) {
+ if (sequence[i + 1] === "x") {
+ bytes.push(parseInt(sequence.substring(i + 2, i + 4), 16));
+ } else {
+ bytes.push(parseInt(sequence.substring(i + 1, i + 4), 8));
+ }
+ }
+ lastAdded = bs + sequence.length;
+ const decoder = new TextDecoder();
+ rv += decoder.decode(new Uint8Array(bytes));
+ return "";
+ });
+ // We have already incremented lastAdded, which means we ignore the
+ // backslash, and we will do something at the next one.
+ break;
+ }
+ }
+ rv += aStr.substring(lastAdded, aStr.length - 1);
+ return rv;
+ },
+
+ parseBridgeLine(line) {
+ if (!line) {
+ return null;
+ }
+ const re =
+ /\s*(?:(?<transport>\S+)\s+)?(?<addr>[0-9a-fA-F\.\[\]\:]+:\d{1,5})(?:\s+(?<id>[0-9a-fA-F]{40}))?(?:\s+(?<args>.+))?/;
+ const match = re.exec(line);
+ if (!match) {
+ throw new Error(`Invalid bridge line: ${line}.`);
+ }
+ const bridge = match.groups;
+ if (!bridge.transport) {
+ bridge.transport = "vanilla";
+ }
+ return bridge;
+ },
+});
diff --git a/toolkit/components/tor-launcher/TorProcess.sys.mjs b/toolkit/components/tor-launcher/TorProcess.sys.mjs
@@ -0,0 +1,393 @@
+/* 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/. */
+
+import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+import { Subprocess } from "resource://gre/modules/Subprocess.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ TorParsers: "resource://gre/modules/TorParsers.sys.mjs",
+});
+
+const TorProcessStatus = Object.freeze({
+ Unknown: 0,
+ Starting: 1,
+ Running: 2,
+ Exited: 3,
+});
+
+const logger = console.createInstance({
+ maxLogLevel: "Info",
+ prefix: "TorProcess",
+});
+
+/**
+ * This class can be used to start a tor daemon instance and receive
+ * notifications when it exits.
+ * It will automatically convert the settings objects into the appropriate
+ * command line arguments.
+ *
+ * It does not offer a way to stop a process because it is supposed to exit
+ * automatically when the owning control port connection is closed.
+ */
+export class TorProcess {
+ #controlSettings;
+ #socksSettings;
+ #exeFile = null;
+ #dataDir = null;
+ #args = [];
+ #subprocess = null;
+ #status = TorProcessStatus.Unknown;
+
+ onExit = _exitCode => {};
+
+ constructor(controlSettings, socksSettings) {
+ if (
+ controlSettings &&
+ !controlSettings.password?.length &&
+ !controlSettings.cookieFilePath
+ ) {
+ throw new Error("Unauthenticated control port is not supported");
+ }
+
+ const checkPort = (port, allowZero) =>
+ port === undefined ||
+ (Number.isInteger(port) &&
+ port < 65535 &&
+ (port > 0 || (allowZero && port === 0)));
+ if (!checkPort(controlSettings?.port, false)) {
+ throw new Error("Invalid control port");
+ }
+ // Port 0 for SOCKS means automatic port.
+ if (!checkPort(socksSettings.port, true)) {
+ throw new Error("Invalid port specified for the SOCKS port");
+ }
+
+ this.#controlSettings = { ...controlSettings };
+ const ipcFileToString = file =>
+ "unix:" + lazy.TorParsers.escapeString(file.path);
+ if (controlSettings.ipcFile) {
+ this.#controlSettings.ipcFile = ipcFileToString(controlSettings.ipcFile);
+ }
+ this.#socksSettings = { ...socksSettings };
+ if (socksSettings.ipcFile) {
+ this.#socksSettings.ipcFile = ipcFileToString(socksSettings.ipcFile);
+ }
+ }
+
+ get isRunning() {
+ return (
+ this.#status === TorProcessStatus.Starting ||
+ this.#status === TorProcessStatus.Running
+ );
+ }
+
+ async start() {
+ if (this.#subprocess) {
+ return;
+ }
+
+ this.#status = TorProcessStatus.Unknown;
+
+ try {
+ this.#makeArgs();
+ this.#addControlPortArgs();
+ this.#addSocksPortArg();
+
+ const pid = Services.appinfo.processID;
+ if (pid !== 0) {
+ this.#args.push("__OwningControllerProcess", pid.toString());
+ }
+
+ if (lazy.TorLauncherUtil.shouldShowNetworkSettings) {
+ this.#args.push("DisableNetwork", "1");
+ }
+
+ this.#status = TorProcessStatus.Starting;
+
+ // useful for simulating slow tor daemon launch
+ const kPrefTorDaemonLaunchDelay = "extensions.torlauncher.launch_delay";
+ const launchDelay = Services.prefs.getIntPref(
+ kPrefTorDaemonLaunchDelay,
+ 0
+ );
+ if (launchDelay > 0) {
+ await new Promise(resolve => setTimeout(() => resolve(), launchDelay));
+ }
+
+ logger.debug(`Starting ${this.#exeFile.path}`, this.#args);
+ const options = {
+ command: this.#exeFile.path,
+ arguments: this.#args,
+ stderr: "stdout",
+ workdir: lazy.TorLauncherUtil.getTorFile("pt-startup-dir", false).path,
+ };
+ if (lazy.TorLauncherUtil.isLinux) {
+ let ldLibPath = Services.env.get("LD_LIBRARY_PATH") ?? "";
+ if (ldLibPath) {
+ ldLibPath = ":" + ldLibPath;
+ }
+ options.environment = {
+ LD_LIBRARY_PATH: this.#exeFile.parent.path + ldLibPath,
+ };
+ options.environmentAppend = true;
+ }
+ this.#subprocess = await Subprocess.call(options);
+ this.#status = TorProcessStatus.Running;
+ } catch (e) {
+ this.#status = TorProcessStatus.Exited;
+ this.#subprocess = null;
+ logger.error("startTor error:", e);
+ throw e;
+ }
+
+ // Do not await the following functions, as they will return only when the
+ // process exits.
+ this.#dumpStdout();
+ this.#watchProcess();
+ }
+
+ // Forget about a process.
+ //
+ // Instead of killing the tor process, we rely on the TAKEOWNERSHIP feature
+ // to shut down tor when we close the control port connection.
+ //
+ // Previously, we sent a SIGNAL HALT command to the tor control port,
+ // but that caused hangs upon exit in the Firefox 24.x based browser.
+ // Apparently, Firefox does not like to process socket I/O while
+ // quitting if the browser did not finish starting up (e.g., when
+ // someone presses the Quit button on our Network Settings window
+ // during startup).
+ //
+ // Still, before closing the owning connection, this class should forget about
+ // the process, so that future notifications will be ignored.
+ forget() {
+ this.#subprocess = null;
+ this.#status = TorProcessStatus.Exited;
+ }
+
+ async #dumpStdout() {
+ let string;
+ while (
+ this.#subprocess &&
+ (string = await this.#subprocess.stdout.readString())
+ ) {
+ dump(string);
+ }
+ }
+
+ async #watchProcess() {
+ const watched = this.#subprocess;
+ if (!watched) {
+ return;
+ }
+ let processExitCode;
+ try {
+ const { exitCode } = await watched.wait();
+ processExitCode = exitCode;
+
+ if (watched !== this.#subprocess) {
+ logger.debug(`A Tor process exited with code ${exitCode}.`);
+ } else if (exitCode) {
+ logger.warn(`The watched Tor process exited with code ${exitCode}.`);
+ } else {
+ logger.info("The Tor process exited.");
+ }
+ } catch (e) {
+ logger.error("Failed to watch the tor process", e);
+ }
+
+ if (watched === this.#subprocess) {
+ this.#processExitedUnexpectedly(processExitCode);
+ }
+ }
+
+ #processExitedUnexpectedly(exitCode) {
+ this.#subprocess = null;
+ this.#status = TorProcessStatus.Exited;
+ logger.warn("Tor exited suddenly.");
+ this.onExit(exitCode);
+ }
+
+ #makeArgs() {
+ this.#exeFile = lazy.TorLauncherUtil.getTorFile("tor", false);
+ if (!this.#exeFile) {
+ throw new Error("Could not find the tor binary.");
+ }
+ const torrcFile = lazy.TorLauncherUtil.getTorFile("torrc", true);
+ if (!torrcFile) {
+ // FIXME: Is this still a fatal error?
+ throw new Error("Could not find the torrc.");
+ }
+ // Get the Tor data directory first so it is created before we try to
+ // construct paths to files that will be inside it.
+ this.#dataDir = lazy.TorLauncherUtil.getTorFile("tordatadir", true);
+ if (!this.#dataDir) {
+ throw new Error("Could not find the tor data directory.");
+ }
+ const onionAuthDir = lazy.TorLauncherUtil.getTorFile(
+ "toronionauthdir",
+ true
+ );
+ if (!onionAuthDir) {
+ throw new Error("Could not find the tor onion authentication directory.");
+ }
+
+ this.#args = [];
+ this.#args.push("-f", torrcFile.path);
+ this.#args.push("DataDirectory", this.#dataDir.path);
+ this.#args.push("ClientOnionAuthDir", onionAuthDir.path);
+
+ // TODO: Create this starting from pt_config.json (tor-browser#42357).
+ const torrcDefaultsFile = lazy.TorLauncherUtil.getTorFile(
+ "torrc-defaults",
+ false
+ );
+ if (torrcDefaultsFile) {
+ this.#args.push("--defaults-torrc", torrcDefaultsFile.path);
+ // The geoip and geoip6 files are in the same directory as torrc-defaults.
+ // TODO: Change TorFile to return the generic path to these files to make
+ // them independent from the torrc-defaults.
+ const geoipFile = torrcDefaultsFile.clone();
+ geoipFile.leafName = "geoip";
+ this.#args.push("GeoIPFile", geoipFile.path);
+ const geoip6File = torrcDefaultsFile.clone();
+ geoip6File.leafName = "geoip6";
+ this.#args.push("GeoIPv6File", geoip6File.path);
+ } else {
+ logger.warn(
+ "torrc-defaults was not found, some functionalities will be disabled."
+ );
+ }
+ }
+
+ /**
+ * Add all the arguments related to the control port.
+ * We use the + prefix so that the the port is added to any other port already
+ * defined in the torrc, and the __ prefix so that it is never written to
+ * torrc.
+ */
+ #addControlPortArgs() {
+ if (!this.#controlSettings) {
+ return;
+ }
+
+ let controlPortArg;
+ if (this.#controlSettings.ipcFile) {
+ controlPortArg = this.#controlSettings.ipcFile;
+ } else if (this.#controlSettings.port) {
+ controlPortArg = this.#controlSettings.host
+ ? `${this.#controlSettings.host}:${this.#controlSettings.port}`
+ : this.#controlSettings.port.toString();
+ }
+ if (controlPortArg) {
+ this.#args.push("+__ControlPort", controlPortArg);
+ }
+
+ if (this.#controlSettings.password?.length) {
+ this.#args.push(
+ "HashedControlPassword",
+ this.#hashPassword(this.#controlSettings.password)
+ );
+ }
+ if (this.#controlSettings.cookieFilePath) {
+ this.#args.push("CookieAuthentication", "1");
+ this.#args.push("CookieAuthFile", this.#controlSettings.cookieFilePath);
+ }
+ }
+
+ /**
+ * Add the argument related to the control port.
+ * We use the + prefix so that the the port is added to any other port already
+ * defined in the torrc, and the __ prefix so that it is never written to
+ * torrc.
+ */
+ #addSocksPortArg() {
+ let socksPortArg;
+ if (this.#socksSettings.ipcFile) {
+ socksPortArg = this.#socksSettings.ipcFile;
+ } else if (this.#socksSettings.port > 0) {
+ socksPortArg = this.#socksSettings.host
+ ? `${this.#socksSettings.host}:${this.#socksSettings.port}`
+ : this.#socksSettings.port.toString();
+ } else {
+ socksPortArg = "auto";
+ }
+ if (socksPortArg) {
+ const socksPortFlags = Services.prefs.getCharPref(
+ "extensions.torlauncher.socks_port_flags",
+ "IPv6Traffic PreferIPv6 KeepAliveIsolateSOCKSAuth"
+ );
+ if (socksPortFlags) {
+ socksPortArg += " " + socksPortFlags;
+ }
+ this.#args.push("+__SocksPort", socksPortArg);
+ }
+ }
+
+ /**
+ * Hash a password to then pass it to Tor as a command line argument.
+ * Based on Vidalia's TorSettings::hashPassword().
+ *
+ * @param {Uint8Array} password The password, as an array of bytes
+ * @returns {string} The hashed password
+ */
+ #hashPassword(password) {
+ // The password has already been checked by the caller.
+
+ // Generate a random, 8 byte salt value.
+ const salt = Array.from(crypto.getRandomValues(new Uint8Array(8)));
+
+ // Run through the S2K algorithm and convert to a string.
+ const toHex = v => v.toString(16).padStart(2, "0");
+ const arrayToHex = aArray => aArray.map(toHex).join("");
+ const kCodedCount = 96;
+ const hashVal = this.#cryptoSecretToKey(
+ Array.from(password),
+ salt,
+ kCodedCount
+ );
+ return "16:" + arrayToHex(salt) + toHex(kCodedCount) + arrayToHex(hashVal);
+ }
+
+ /**
+ * Generates and return a hash of a password by following the iterated and
+ * salted S2K algorithm (see RFC 2440 section 3.6.1.3).
+ * See also https://gitlab.torproject.org/tpo/core/torspec/-/blob/main/control-spec.txt#L3824.
+ * #cryptoSecretToKey() is similar to Vidalia's crypto_secret_to_key().
+ *
+ * @param {Array} password The password to hash, as an array of bytes
+ * @param {Array} salt The salt to use for the hash, as an array of bytes
+ * @param {number} codedCount The counter, coded as specified in RFC 2440
+ * @returns {Array} The hash of the password, as an array of bytes
+ */
+ #cryptoSecretToKey(password, salt, codedCount) {
+ const inputArray = salt.concat(password);
+
+ // Subtle crypto only has the final digest, and does not allow incremental
+ // updates.
+ const hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA1);
+ const kEXPBIAS = 6;
+ let count = (16 + (codedCount & 15)) << ((codedCount >> 4) + kEXPBIAS);
+ while (count > 0) {
+ if (count > inputArray.length) {
+ hasher.update(inputArray, inputArray.length);
+ count -= inputArray.length;
+ } else {
+ const finalArray = inputArray.slice(0, count);
+ hasher.update(finalArray, finalArray.length);
+ count = 0;
+ }
+ }
+ return hasher
+ .finish(false)
+ .split("")
+ .map(b => b.charCodeAt(0));
+ }
+}
diff --git a/toolkit/components/tor-launcher/TorProcessAndroid.sys.mjs b/toolkit/components/tor-launcher/TorProcessAndroid.sys.mjs
@@ -0,0 +1,131 @@
+/* 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, {
+ EventDispatcher: "resource://gre/modules/Messaging.sys.mjs",
+});
+
+const logger = console.createInstance({
+ maxLogLevel: "Info",
+ prefix: "TorProcessAndroid",
+});
+
+const TorOutgoingEvents = Object.freeze({
+ start: "GeckoView:Tor:StartTor",
+ stop: "GeckoView:Tor:StopTor",
+});
+
+// The events we will listen to
+const TorIncomingEvents = Object.freeze({
+ started: "GeckoView:Tor:TorStarted",
+ startFailed: "GeckoView:Tor:TorStartFailed",
+ exited: "GeckoView:Tor:TorExited",
+});
+
+/**
+ * This class allows to start a tor process on Android devices.
+ *
+ * GeckoView does not include the facilities to start processes from JavaScript,
+ * therefore the actual implementation is written in Java, and this is just
+ * plumbing code over the global EventDispatcher.
+ */
+export class TorProcessAndroid {
+ /**
+ * The handle the Java counterpart uses to refer to the process we started.
+ * We use it to filter the exit events and make sure they refer to the daemon
+ * we are interested in.
+ */
+ #processHandle = null;
+ /**
+ * The promise resolver we call when the Java counterpart sends the event that
+ * tor has started.
+ */
+ #startResolve = null;
+ /**
+ * The promise resolver we call when the Java counterpart sends the event that
+ * it failed to start tor.
+ */
+ #startReject = null;
+
+ onExit = () => {};
+
+ get isRunning() {
+ return !!this.#processHandle;
+ }
+
+ async start() {
+ // Generate the handle on the JS side so that it's ready in case it takes
+ // less to start the process than to propagate the success.
+ this.#processHandle = crypto.randomUUID();
+ logger.info(`Starting new process with handle ${this.#processHandle}`);
+ // Let's declare it immediately, so that the Java side can do its stuff in
+ // an async manner and we avoid possible race conditions (at most we await
+ // an already resolved/rejected promise.
+ const startEventPromise = new Promise((resolve, reject) => {
+ this.#startResolve = resolve;
+ this.#startReject = reject;
+ });
+ lazy.EventDispatcher.instance.registerListener(
+ this,
+ Object.values(TorIncomingEvents)
+ );
+ let config;
+ try {
+ config = await lazy.EventDispatcher.instance.sendRequestForResult({
+ type: TorOutgoingEvents.start,
+ handle: this.#processHandle,
+ tcpSocks: Services.prefs.getBoolPref(
+ "extensions.torlauncher.socks_port_use_tcp",
+ false
+ ),
+ });
+ logger.debug("Sent the start event.");
+ } catch (e) {
+ this.forget();
+ throw e;
+ }
+ await startEventPromise;
+ return config;
+ }
+
+ forget() {
+ // Processes usually exit when we close the control port connection to them.
+ logger.trace(`Forgetting process ${this.#processHandle}`);
+ lazy.EventDispatcher.instance.sendRequestForResult({
+ type: TorOutgoingEvents.stop,
+ handle: this.#processHandle,
+ });
+ logger.debug("Sent the start event.");
+ this.#processHandle = null;
+ lazy.EventDispatcher.instance.unregisterListener(
+ this,
+ Object.values(TorIncomingEvents)
+ );
+ }
+
+ onEvent(event, data, _callback) {
+ if (data?.handle !== this.#processHandle) {
+ logger.debug(`Ignoring event ${event} with another handle`, data);
+ return;
+ }
+ logger.info(`Received an event ${event}`, data);
+ switch (event) {
+ case TorIncomingEvents.started:
+ this.#startResolve();
+ break;
+ case TorIncomingEvents.startFailed:
+ this.#startReject(new Error(data.error));
+ break;
+ case TorIncomingEvents.exited:
+ this.forget();
+ if (this.#startReject !== null) {
+ this.#startReject();
+ }
+ this.onExit(data.status);
+ break;
+ }
+ }
+}
diff --git a/toolkit/components/tor-launcher/TorProvider.sys.mjs b/toolkit/components/tor-launcher/TorProvider.sys.mjs
@@ -0,0 +1,1148 @@
+/* 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/. */
+
+import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs";
+import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
+import {
+ TorBootstrapError,
+ TorProviderTopics,
+} from "resource://gre/modules/TorProviderBuilder.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ TorController: "resource://gre/modules/TorControlPort.sys.mjs",
+ TorProcess: "resource://gre/modules/TorProcess.sys.mjs",
+ TorProcessAndroid: "resource://gre/modules/TorProcessAndroid.sys.mjs",
+ TorProxyType: "resource://gre/modules/TorSettings.sys.mjs",
+ TorSettings: "resource://gre/modules/TorSettings.sys.mjs",
+});
+
+const logger = console.createInstance({
+ maxLogLevelPref: "browser.tor_provider.log_level",
+ prefix: "TorProvider",
+});
+
+/**
+ * @typedef {object} ControlPortSettings An object with the settings to use for
+ * the control port. All the entries are optional, but an authentication
+ * mechanism and a communication method must be specified.
+ * @property {Uint8Array} [password] The clear text password as an array of
+ * bytes. It must always be defined, unless cookieFilePath is
+ * @property {string} [cookieFilePath] The path to the cookie file to use for
+ * authentication
+ * @property {nsIFile} [ipcFile] The nsIFile object with the path to a Unix
+ * socket to use for control socket
+ * @property {string} [host] The host to connect for a TCP control port
+ * @property {number} [port] The port number to use for a TCP control port
+ */
+/**
+ * @typedef {object} SocksSettings An object that includes the proxy settings to
+ * be configured in the browser.
+ * @property {boolean} [transproxy] If true, no proxy is configured
+ * @property {nsIFile} [ipcFile] The nsIFile object with the path to a Unix
+ * socket to use for an IPC proxy
+ * @property {string} [host] The host to connect for a TCP proxy
+ * @property {number} [port] The port number to use for a TCP proxy
+ */
+/**
+ * Stores the data associated with a circuit node.
+ *
+ * @typedef NodeData
+ * @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
+ * node
+ * @property {string?} regionCode An upper case 2-letter ISO3166-1 code for the
+ * first ip address, or null if there is no region. This should also be a
+ * valid BCP47 Region subtag
+ */
+
+const Preferences = Object.freeze({
+ ControlUseIpc: "extensions.torlauncher.control_port_use_ipc",
+ ControlHost: "extensions.torlauncher.control_host",
+ ControlPort: "extensions.torlauncher.control_port",
+});
+
+/* Config Keys used to configure tor daemon */
+const TorConfigKeys = Object.freeze({
+ useBridges: "UseBridges",
+ bridgeList: "Bridge",
+ socks4Proxy: "Socks4Proxy",
+ socks5Proxy: "Socks5Proxy",
+ socks5ProxyUsername: "Socks5ProxyUsername",
+ socks5ProxyPassword: "Socks5ProxyPassword",
+ httpsProxy: "HTTPSProxy",
+ httpsProxyAuthenticator: "HTTPSProxyAuthenticator",
+ reachableAddresses: "ReachableAddresses",
+ clientTransportPlugin: "ClientTransportPlugin",
+});
+
+/**
+ * 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.
+ */
+export class TorProvider {
+ /**
+ * The control port settings.
+ *
+ * @type {ControlPortSettings?}
+ */
+ #controlPortSettings = null;
+ /**
+ * An instance of the tor controller.
+ * We take for granted that if it is not null, we connected to it and managed
+ * to authenticate.
+ * Public methods can use the #controller getter, which will throw an
+ * exception whenever the control port is not open.
+ *
+ * @type {TorController?}
+ */
+ #controlConnection = null;
+ /**
+ * A helper that can be used to get the control port connection and assert it
+ * is open and it can be used.
+ * If this is not the case, this getter will throw.
+ *
+ * @returns {TorController}
+ */
+ get #controller() {
+ if (!this.#controlConnection?.isOpen) {
+ throw new Error("Control port connection not available.");
+ }
+ return this.#controlConnection;
+ }
+ /**
+ * A function that can be called to cancel the current connection attempt.
+ */
+ #cancelConnection = () => {};
+
+ /**
+ * The tor process we launched.
+ *
+ * @type {TorProcess}
+ */
+ #torProcess = null;
+
+ /**
+ * The settings for the SOCKS proxy.
+ *
+ * @type {SocksSettings?}
+ */
+ #socksSettings = null;
+
+ #isBootstrapDone = false;
+ /**
+ * Keep the last warning to avoid broadcasting an async warning if it is the
+ * same one as the last broadcast.
+ */
+ #lastWarning = {};
+
+ /**
+ * 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, Promise<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;
+
+ /**
+ * Starts a new tor process and connect to its control port, or connect to the
+ * control port of an existing tor daemon.
+ */
+ async init() {
+ logger.debug("Initializing the Tor provider.");
+
+ // These settings might be customized in the following steps.
+ if (TorLauncherUtil.isAndroid) {
+ this.#socksSettings = { transproxy: false };
+ } else {
+ this.#socksSettings = TorLauncherUtil.getPreferredSocksConfiguration();
+ logger.debug("Requested SOCKS configuration", this.#socksSettings);
+ }
+
+ try {
+ await this.#setControlPortConfiguration();
+ } catch (e) {
+ logger.error("We do not have a control port configuration", e);
+ throw e;
+ }
+
+ if (this.#socksSettings.transproxy) {
+ logger.info("Transparent proxy required, not starting a Tor daemon.");
+ } else if (this.ownsTorDaemon) {
+ try {
+ await this.#startDaemon();
+ } catch (e) {
+ logger.error("Failed to start the tor daemon", e);
+ throw e;
+ }
+ } else {
+ logger.debug(
+ "Not starting a tor daemon because we were requested not to."
+ );
+ }
+
+ try {
+ await this.#firstConnection();
+ } catch (e) {
+ logger.error("Cannot connect to the control port", e);
+ throw e;
+ }
+
+ if (this.ownsTorDaemon) {
+ try {
+ await lazy.TorSettings.initializedPromise;
+ await lazy.TorSettings.setTorProvider(this);
+ } catch (e) {
+ logger.warn(
+ "Failed to initialize TorSettings or to write our initial settings. Continuing the initialization anyway.",
+ e
+ );
+ }
+ }
+
+ TorLauncherUtil.setProxyConfiguration(this.#socksSettings);
+
+ logger.info("The Tor provider is ready.");
+
+ // If we are using an external Tor daemon, we might need to fetch circuits
+ // already, in case streams use them. Do not await because we do not want to
+ // block the intialization on this (it should not fail anyway...).
+ this.#fetchCircuits();
+ }
+
+ /**
+ * Close the connection to the tor daemon.
+ * 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() {
+ logger.debug("Uninitializing the Tor provider.");
+
+ if (this.#torProcess) {
+ this.#torProcess.forget();
+ this.#torProcess.onExit = () => {};
+ this.#torProcess = null;
+ }
+
+ this.#closeConnection("Uninitializing the provider.");
+ }
+
+ // Provider API
+
+ /**
+ * Send settings to the tor daemon.
+ *
+ * @param {Map<string, ?string>} torSettings - The key value pairs to pass in.
+ */
+ async #writeSettings(torSettings) {
+ logger.debug("Mapped settings object", torSettings);
+
+ // NOTE: The order in which TorProvider.#writeSettings should match the
+ // order in which the configuration is passed onto setConf. In turn,
+ // TorControlPort.setConf should similarly ensure that the configuration
+ // reaches the tor process in the same order.
+ // In particular, we do not want a race where an earlier call to
+ // TorProvider.#writeSettings for overlapping settings can be delayed and
+ // override a later call.
+ await this.#controller.setConf(Array.from(torSettings));
+ }
+
+ /**
+ * Send bridge settings to the tor daemon.
+ *
+ * This should only be called by the `TorSettings` module.
+ *
+ * @param {TorBridgeSettings} bridges - The bridge settings to apply.
+ */
+ async writeBridgeSettings(bridges) {
+ logger.debug("TorProvider.writeBridgeSettings", bridges);
+ const torSettings = new Map();
+
+ // Bridges
+ const haveBridges = bridges?.enabled && !!bridges.bridge_strings.length;
+ torSettings.set(TorConfigKeys.useBridges, haveBridges);
+ torSettings.set(
+ TorConfigKeys.bridgeList,
+ haveBridges ? bridges.bridge_strings : null
+ );
+
+ await this.#writeSettings(torSettings);
+ }
+
+ /**
+ * Send proxy settings to the tor daemon.
+ *
+ * This should only be called by the `TorSettings` module.
+ *
+ * @param {TorProxySettings} proxy - The proxy settings to apply.
+ */
+ async writeProxySettings(proxy) {
+ logger.debug("TorProvider.writeProxySettings", proxy);
+ const torSettings = new Map();
+
+ torSettings.set(TorConfigKeys.socks4Proxy, null);
+ torSettings.set(TorConfigKeys.socks5Proxy, null);
+ torSettings.set(TorConfigKeys.socks5ProxyUsername, null);
+ torSettings.set(TorConfigKeys.socks5ProxyPassword, null);
+ torSettings.set(TorConfigKeys.httpsProxy, null);
+ torSettings.set(TorConfigKeys.httpsProxyAuthenticator, null);
+
+ const type = proxy.enabled ? proxy.type : null;
+ const { address, port, username, password } = proxy;
+
+ switch (type) {
+ case lazy.TorProxyType.Socks4:
+ torSettings.set(TorConfigKeys.socks4Proxy, `${address}:${port}`);
+ break;
+ case lazy.TorProxyType.Socks5:
+ torSettings.set(TorConfigKeys.socks5Proxy, `${address}:${port}`);
+ torSettings.set(TorConfigKeys.socks5ProxyUsername, username);
+ torSettings.set(TorConfigKeys.socks5ProxyPassword, password);
+ break;
+ case lazy.TorProxyType.HTTPS:
+ torSettings.set(TorConfigKeys.httpsProxy, `${address}:${port}`);
+ torSettings.set(
+ TorConfigKeys.httpsProxyAuthenticator,
+ `${username}:${password}`
+ );
+ break;
+ }
+
+ await this.#writeSettings(torSettings);
+ }
+
+ /**
+ * Send firewall settings to the tor daemon.
+ *
+ * This should only be called by the `TorSettings` module.
+ *
+ * @param {TorFirewallSettings} firewall - The firewall settings to apply.
+ */
+ async writeFirewallSettings(firewall) {
+ logger.debug("TorProvider.writeFirewallSettings", firewall);
+ const torSettings = new Map();
+
+ torSettings.set(
+ TorConfigKeys.reachableAddresses,
+ firewall.enabled
+ ? firewall.allowed_ports.map(port => `*:${port}`).join(",")
+ : null
+ );
+
+ await this.#writeSettings(torSettings);
+ }
+
+ async flushSettings() {
+ if (TorLauncherUtil.isAndroid) {
+ // Android does not have a torrc to flush to. See tor-browser#43577.
+ return;
+ }
+ await this.#controller.flushSettings();
+ }
+
+ /**
+ * Start the bootstrap process.
+ */
+ async connect() {
+ await this.#controller.setNetworkEnabled(true);
+ if (this.#socksSettings.port === 0) {
+ // Enablign/disabling network resets also the SOCKS listener.
+ // So, every time we do it, we need to update the browser's configuration
+ // to use the updated port.
+ const settings = structuredClone(this.#socksSettings);
+ for (const listener of await this.#controller.getSocksListeners()) {
+ // When set to automatic port, ignore any IPC listener, as the intention
+ // was to use TCP.
+ if (listener.ipcPath) {
+ continue;
+ }
+ // The tor daemon can have any number of SOCKS listeners (see SocksPort
+ // in man 1 tor). We take for granted that any TCP one will work for us.
+ settings.host = listener.host;
+ settings.port = listener.port;
+ break;
+ }
+ TorLauncherUtil.setProxyConfiguration(settings);
+ }
+ this.#lastWarning = {};
+ this.retrieveBootstrapStatus();
+ }
+
+ /**
+ * Stop the bootstrap process.
+ */
+ async stopBootstrap() {
+ // Tell tor to disable use of the network; this should stop the bootstrap.
+ await this.#controller.setNetworkEnabled(false);
+ // 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.
+ this.retrieveBootstrapStatus();
+ }
+
+ /**
+ * Ask Tor to swtich to new circuits and clear the DNS cache.
+ */
+ async newnym() {
+ await this.#controller.newnym();
+ }
+
+ /**
+ * Get the bridges Tor has been configured with.
+ *
+ * @returns {Bridge[]} The configured bridges
+ */
+ async getBridges() {
+ // Ideally, we would not need this function, because we should be the one
+ // setting them with TorSettings. However, TorSettings is not notified of
+ // change of settings. So, asking tor directly with the control connection
+ // 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.#controller.getBridges();
+ }
+
+ /**
+ * Get the configured pluggable transports.
+ *
+ * @returns {PTInfo[]} An array with the info of all the configured pluggable
+ * transports.
+ */
+ async getPluggableTransports() {
+ return this.#controller.getPluggableTransports();
+ }
+
+ /**
+ * Ask Tor its bootstrap phase.
+ * This function will also update the internal state when using an external
+ * tor daemon.
+ *
+ * @returns {object} An object with the bootstrap information received from
+ * Tor. Its keys might vary, depending on the input
+ */
+ async retrieveBootstrapStatus() {
+ this.#processBootstrapStatus(
+ await this.#controller.getBootstrapPhase(),
+ false
+ );
+ }
+
+ /**
+ * Returns tha data about a relay or a bridge.
+ *
+ * @param {string} id The fingerprint of the node to get data about
+ * @returns {Promise<NodeData>}
+ */
+ async getNodeInfo(id) {
+ const node = {
+ fingerprint: id,
+ ipAddrs: [],
+ bridgeType: null,
+ regionCode: null,
+ };
+ const bridge = (await this.#controller.getBridges())?.find(
+ foundBridge => foundBridge.id?.toUpperCase() === id.toUpperCase()
+ );
+ if (bridge) {
+ node.bridgeType = bridge.transport ?? "";
+ // Attempt to get an IP address from bridge address string.
+ const ip = bridge.addr.match(/^\[?([^\]]+)\]?:\d+$/)?.[1];
+ if (ip && !ip.startsWith("0.")) {
+ node.ipAddrs.push(ip);
+ }
+ } else {
+ node.ipAddrs = await this.#controller.getNodeAddresses(id);
+ }
+ // tor-browser#43116, tor-browser-build#41224: on Android, we do not ship
+ // the GeoIP databases to save some space. So skip it for now.
+ if (node.ipAddrs.length && !TorLauncherUtil.isAndroid) {
+ // Get the country code for the node's IP address.
+ try {
+ // Expect a 2-letter ISO3166-1 code, which should also be a valid
+ // BCP47 Region subtag.
+ const regionCode = await this.#controller.getIPCountry(node.ipAddrs[0]);
+ if (regionCode && regionCode !== "??") {
+ node.regionCode = regionCode.toUpperCase();
+ }
+ } catch (e) {
+ logger.warn(`Cannot get a country for IP ${node.ipAddrs[0]}`, e);
+ }
+ }
+ return node;
+ }
+
+ /**
+ * Add a private key to the Tor configuration.
+ *
+ * @param {string} address The address of the onion service
+ * @param {string} b64PrivateKey The private key of the service, in base64
+ * @param {boolean} isPermanent Tell whether the key should be saved forever
+ */
+ async onionAuthAdd(address, b64PrivateKey, isPermanent) {
+ await this.#controller.onionAuthAdd(address, b64PrivateKey, isPermanent);
+ }
+
+ /**
+ * Remove a private key from the Tor configuration.
+ *
+ * @param {string} address The address of the onion service
+ */
+ async onionAuthRemove(address) {
+ await this.#controller.onionAuthRemove(address);
+ }
+
+ /**
+ * Retrieve the list of private keys.
+ *
+ * @returns {OnionAuthKeyInfo[]} The onion authentication keys known by the
+ * tor daemon
+ */
+ async onionAuthViewKeys() {
+ return this.#controller.onionAuthViewKeys();
+ }
+
+ /**
+ * @returns {boolean} true if we launched and control tor, false if we are
+ * using system tor.
+ */
+ get ownsTorDaemon() {
+ return TorLauncherUtil.shouldStartAndOwnTor;
+ }
+
+ get isBootstrapDone() {
+ return this.#isBootstrapDone;
+ }
+
+ /**
+ * TODO: Rename to isReady once we remove finish the migration.
+ *
+ * @returns {boolean} true if we currently have a connection to the control
+ * port. We take for granted that if we have one, we authenticated to it, and
+ * so we have already verified we can send and receive data.
+ */
+ get isRunning() {
+ return this.#controlConnection?.isOpen ?? false;
+ }
+
+ /**
+ * 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;
+ }
+
+ // Process management
+
+ async #startDaemon() {
+ // TorProcess should be instanced once, then always reused and restarted
+ // only through the prompt it exposes when the controlled process dies.
+ if (this.#torProcess) {
+ logger.warn(
+ "Ignoring a request to start a tor daemon because one is already running."
+ );
+ return;
+ }
+
+ if (TorLauncherUtil.isAndroid) {
+ this.#torProcess = new lazy.TorProcessAndroid();
+ } else {
+ this.#torProcess = new lazy.TorProcess(
+ this.#controlPortSettings,
+ this.#socksSettings
+ );
+ }
+ // Use a closure instead of bind because we reassign #cancelConnection.
+ // Also, we now assign an exit handler that cancels the first connection,
+ // so that a sudden exit before the first connection is completed might
+ // still be handled as an initialization failure.
+ // But after the first connection is created successfully, we will change
+ // the exit handler to broadcast a notification instead.
+ this.#torProcess.onExit = () => {
+ this.#cancelConnection(
+ "The tor process exited before the first connection"
+ );
+ };
+
+ logger.debug("Trying to start the tor process.");
+ const res = await this.#torProcess.start();
+ if (TorLauncherUtil.isAndroid) {
+ logger.debug("Configuration from TorProcessAndriod", res);
+ this.#controlPortSettings = {
+ ipcFile: new lazy.FileUtils.File(res.controlPortPath),
+ cookieFilePath: res.cookieFilePath,
+ };
+ this.#socksSettings = {
+ transproxy: false,
+ };
+ if (res.socksPath) {
+ this.#socksSettings.ipcFile = new lazy.FileUtils.File(res.socksPath);
+ } else if (res.socksPort !== undefined) {
+ this.#socksSettings.host = res.socksHost ?? "127.0.0.1";
+ this.#socksSettings.port = res.socksPort;
+ } else {
+ throw new Error(
+ "TorProcessAndroid did not return a valid SOCKS configuration."
+ );
+ }
+ }
+ logger.info("Started a tor process");
+ }
+
+ // Control port setup and connection
+
+ /**
+ * Read the control port settings from environment variables and from
+ * preferences.
+ */
+ async #setControlPortConfiguration() {
+ logger.debug("Reading the control port configuration");
+ const settings = {};
+
+ if (TorLauncherUtil.isAndroid) {
+ // We will populate the settings after having started the daemon.
+ return;
+ }
+
+ const isWindows = Services.appinfo.OS === "WINNT";
+ // 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");
+ settings.ipcFile = new lazy.FileUtils.File(ipcPath);
+ } else {
+ // Check for TCP host and port environment variables.
+ if (Services.env.exists("TOR_CONTROL_HOST")) {
+ settings.host = Services.env.get("TOR_CONTROL_HOST");
+ }
+ if (Services.env.exists("TOR_CONTROL_PORT")) {
+ const port = parseInt(Services.env.get("TOR_CONTROL_PORT"), 10);
+ if (Number.isInteger(port) && port > 0 && port <= 65535) {
+ settings.port = port;
+ }
+ }
+ }
+
+ const useIPC =
+ !isWindows &&
+ Services.prefs.getBoolPref(Preferences.ControlUseIpc, false);
+ if (!settings.host && !settings.port && useIPC) {
+ settings.ipcFile = TorLauncherUtil.getTorFile("control_ipc", false);
+ } else {
+ if (!settings.host) {
+ settings.host = Services.prefs.getCharPref(
+ Preferences.ControlHost,
+ "127.0.0.1"
+ );
+ }
+ if (!settings.port) {
+ settings.port = Services.prefs.getIntPref(
+ Preferences.ControlPort,
+ 9151
+ );
+ }
+ }
+
+ if (Services.env.exists("TOR_CONTROL_PASSWD")) {
+ const password = Services.env.get("TOR_CONTROL_PASSWD");
+ // As per 3.5 of control-spec.txt, AUTHENTICATE can use either a quoted
+ // string, or a sequence of hex characters.
+ // However, the password is hashed byte by byte, so we need to convert the
+ // string to its character codes, or the hex digits to actual bytes.
+ // Notice that Tor requires at least one hex character, without an upper
+ // limit, but it does not explicitly tell how to pad an odd number of hex
+ // characters, so we require the user to hand an even number of hex
+ // digits.
+ // We also want to enforce the authentication if we start the daemon.
+ // So, if a password is not valid (not a hex sequence and not a quoted
+ // string), or if it is empty (including the quoted empty string), we
+ // force a random password.
+ if (
+ password.length >= 2 &&
+ password[0] === '"' &&
+ password[password.length - 1] === '"'
+ ) {
+ const encoder = new TextEncoder();
+ settings.password = encoder.encode(TorParsers.unescapeString(password));
+ } else if (/^([0-9a-fA-F]{2})+$/.test(password)) {
+ settings.password = new Uint8Array(password.length / 2);
+ for (let i = 0, j = 0; i < settings.password.length; i++, j += 2) {
+ settings.password[i] = parseInt(password.substring(j, j + 2), 16);
+ }
+ }
+ if (password && !settings.password?.length) {
+ logger.warn(
+ "Invalid password specified at TOR_CONTROL_PASSWD. " +
+ "You should put it in double quotes, or it should be a hex-encoded sequence. " +
+ "The password cannot be empty. " +
+ "A random password will be used, instead."
+ );
+ }
+ } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
+ const cookiePath = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
+ if (cookiePath) {
+ settings.cookieFilePath = cookiePath;
+ }
+ }
+ if (
+ this.ownsTorDaemon &&
+ !settings.password?.length &&
+ !settings.cookieFilePath
+ ) {
+ settings.password = this.#generateRandomPassword();
+ }
+ this.#controlPortSettings = settings;
+ logger.debug("Control port configuration read");
+ }
+
+ /**
+ * Start the first connection to the Tor daemon.
+ * This function should be called only once during the initialization.
+ */
+ async #firstConnection() {
+ let canceled = false;
+ let timeout = 0;
+ const maxDelay = 10_000;
+ let delay = 5;
+ logger.debug("Connecting to the control port for the first time.");
+ this.#controlConnection = await new Promise((resolve, reject) => {
+ this.#cancelConnection = reason => {
+ canceled = true;
+ clearTimeout(timeout);
+ reject(new Error(reason));
+ };
+ const tryConnect = () => {
+ if (this.ownsTorDaemon && !this.#torProcess?.isRunning) {
+ reject(new Error("The controlled tor daemon is not running."));
+ return;
+ }
+ this.#openControlPort()
+ .then(controller => {
+ this.#cancelConnection = () => {};
+ // The cancel function should have already called reject.
+ if (!canceled) {
+ logger.info("Connected to the control port.");
+ resolve(controller);
+ }
+ })
+ .catch(e => {
+ if (delay < maxDelay && !canceled) {
+ logger.info(
+ `Failed to connect to the control port. Trying again in ${delay}ms.`,
+ e
+ );
+ timeout = setTimeout(tryConnect, delay);
+ delay *= 2;
+ } else {
+ reject(e);
+ }
+ });
+ };
+ tryConnect();
+ });
+
+ // The following code will never throw, but we still want to wait for it
+ // before marking the provider as initialized.
+
+ if (this.ownsTorDaemon) {
+ // The first connection cannot be canceled anymore, and the rest of the
+ // code is supposed not to fail. If the tor process exits, from now on we
+ // can only close the connection and broadcast a notification.
+ this.#torProcess.onExit = exitCode => {
+ logger.info(`The tor process exited with code ${exitCode}`);
+ this.#closeConnection("The tor process exited suddenly");
+ Services.obs.notifyObservers(null, TorProviderTopics.ProcessExited);
+ };
+ if (!TorLauncherUtil.shouldOnlyConfigureTor) {
+ await this.#takeOwnership();
+ }
+ }
+ await this.#setupEvents();
+ }
+
+ /**
+ * Try to become the primary controller. This will make tor exit when our
+ * connection is closed.
+ * This function cannot fail or throw (any exception will be treated as a
+ * warning and just logged).
+ */
+ async #takeOwnership() {
+ logger.debug("Taking the ownership of the tor process.");
+ try {
+ await this.#controlConnection.takeOwnership();
+ } catch (e) {
+ logger.warn("Take ownership failed", e);
+ return;
+ }
+ try {
+ await this.#controlConnection.resetOwningControllerProcess();
+ } catch (e) {
+ logger.warn("Clear owning controller process failed", e);
+ }
+ }
+
+ /**
+ * Tells the Tor daemon which events we want to receive.
+ * This function will never throw. Any failure will be treated as a warning of
+ * a possibly degraded experience, not as an error.
+ */
+ async #setupEvents() {
+ // We always listen to these events, because they are needed for the circuit
+ // display.
+ const events = ["CIRC", "STREAM"];
+ if (this.ownsTorDaemon) {
+ events.push("STATUS_CLIENT", "NOTICE", "WARN", "ERR");
+ // Do not await on the first bootstrap status retrieval, and do not
+ // propagate its errors.
+ this.#controlConnection
+ .getBootstrapPhase()
+ .then(status => this.#processBootstrapStatus(status, false))
+ .catch(e =>
+ logger.error("Failed to get the first bootstrap status", e)
+ );
+ }
+ try {
+ logger.debug(`Setting events: ${events.join(" ")}`);
+ await this.#controlConnection.setEvents(events);
+ } catch (e) {
+ logger.error(
+ "We could not enable all the events we need. Tor Browser's functionalities might be reduced.",
+ e
+ );
+ }
+ }
+
+ /**
+ * Open a connection to the control port and authenticate to it.
+ * #setControlPortConfiguration must have been called before, as this function
+ * will follow the configuration set by it.
+ *
+ * @returns {Promise<TorController>} An authenticated TorController
+ */
+ async #openControlPort() {
+ let controlPort;
+ if (this.#controlPortSettings.ipcFile) {
+ controlPort = lazy.TorController.fromIpcFile(
+ this.#controlPortSettings.ipcFile,
+ this
+ );
+ } else {
+ controlPort = lazy.TorController.fromSocketAddress(
+ this.#controlPortSettings.host,
+ this.#controlPortSettings.port,
+ this
+ );
+ }
+ try {
+ let password = this.#controlPortSettings.password;
+ if (password === undefined && this.#controlPortSettings.cookieFilePath) {
+ password = await this.#readAuthenticationCookie(
+ this.#controlPortSettings.cookieFilePath
+ );
+ }
+ await controlPort.authenticate(password);
+ } catch (e) {
+ try {
+ controlPort.close();
+ } catch (ec) {
+ // Tor already closes the control port when the authentication fails.
+ logger.debug(
+ "Expected exception when closing the control port for a failed authentication",
+ ec
+ );
+ }
+ throw e;
+ }
+ return controlPort;
+ }
+
+ /**
+ * Close the connection to the control port.
+ *
+ * @param {string} reason The reason for which we are closing the connection
+ * (used for logging and in case this ends up canceling the current connection
+ * attempt)
+ */
+ #closeConnection(reason) {
+ this.#cancelConnection(reason);
+ if (this.#controlConnection) {
+ logger.info("Closing the control connection", reason);
+ try {
+ this.#controlConnection.close();
+ } catch (e) {
+ logger.error("Failed to close the control port connection", e);
+ }
+ this.#controlConnection = null;
+ } else {
+ logger.trace(
+ "Requested to close an already closed control port connection"
+ );
+ }
+ this.#isBootstrapDone = false;
+ this.#lastWarning = {};
+ }
+
+ // Authentication
+
+ /**
+ * Read a cookie file to perform cookie-based authentication.
+ *
+ * @param {string} path The path to the cookie file
+ * @returns {Uint8Array} The content of the file in bytes
+ */
+ async #readAuthenticationCookie(path) {
+ return IOUtils.read(path);
+ }
+
+ /**
+ * @returns {Uint8Array} A random 16-byte password.
+ */
+ #generateRandomPassword() {
+ const kPasswordLen = 16;
+ return crypto.getRandomValues(new Uint8Array(kPasswordLen));
+ }
+
+ /**
+ * Ask Tor the circuits it already knows to populate our circuit map with the
+ * circuits that were already open before we started listening for events.
+ */
+ async #fetchCircuits() {
+ for (const { id, nodes } of await this.#controller.getCircuits()) {
+ this.onCircuitBuilt(id, nodes);
+ }
+ }
+
+ // Notification handlers
+
+ /**
+ * Receive and process a notification with the bootstrap status.
+ *
+ * @param {object} status The status object
+ */
+ onBootstrapStatus(status) {
+ logger.debug("Received bootstrap status update", status);
+ this.#processBootstrapStatus(status, true);
+ }
+
+ /**
+ * Process a bootstrap status to update the current state, and broadcast it
+ * to TorBootstrapStatus observers.
+ *
+ * @param {object} statusObj The status object that the controller returned.
+ * Its entries depend on what Tor sent to us.
+ * @param {boolean} isNotification We broadcast warnings only when we receive
+ * them through an asynchronous notification.
+ */
+ #processBootstrapStatus(statusObj, isNotification) {
+ // Notify observers
+ Services.obs.notifyObservers(statusObj, TorProviderTopics.BootstrapStatus);
+
+ if (statusObj.PROGRESS === 100) {
+ this.#isBootstrapDone = true;
+ return;
+ }
+
+ this.#isBootstrapDone = false;
+
+ // Can TYPE ever be ERR for STATUS_CLIENT?
+ if (
+ isNotification &&
+ statusObj.TYPE === "WARN" &&
+ statusObj.RECOMMENDATION !== "ignore"
+ ) {
+ this.#notifyBootstrapError(statusObj);
+ }
+ }
+
+ /**
+ * Broadcast a bootstrap warning or error.
+ *
+ * @param {object} statusObj The bootstrap status object with the error
+ */
+ #notifyBootstrapError(statusObj) {
+ logger.error("Tor bootstrap error", statusObj);
+
+ if (
+ statusObj.TAG !== this.#lastWarning.phase ||
+ statusObj.REASON !== this.#lastWarning.reason
+ ) {
+ this.#lastWarning.phase = statusObj.TAG;
+ this.#lastWarning.reason = statusObj.REASON;
+
+ // FIXME: currently, this is observed only by TorBoostrapRequest.
+ // We should remove that class, and use an async method to do the
+ // bootstrap here.
+ // At that point, the lastWarning mechanism will probably not be necessary
+ // anymore, since the first error eligible for notification will as a
+ // matter of fact cancel the bootstrap.
+ Services.obs.notifyObservers(
+ new TorBootstrapError({
+ phase: statusObj.TAG,
+ reason: statusObj.REASON,
+ summary: statusObj.SUMMARY,
+ }),
+ TorProviderTopics.BootstrapError
+ );
+ }
+ }
+
+ /**
+ * Handle a log message from the tor daemon. It will be added to the internal
+ * logs. If it is a warning or an error, a notification will be broadcast.
+ *
+ * @param {string} type The message type
+ * @param {string} msg The message
+ */
+ onLogMessage(type, msg) {
+ if (type === "WARN" || type === "ERR") {
+ // Notify so that Copy Log can be enabled.
+ Services.obs.notifyObservers(null, TorProviderTopics.HasWarnOrErr);
+ }
+
+ const timestamp = new Date()
+ .toISOString()
+ .replace("T", " ")
+ .replace("Z", "");
+
+ Services.obs.notifyObservers(
+ { type, msg, timestamp },
+ TorProviderTopics.TorLog
+ );
+
+ switch (type) {
+ case "ERR":
+ logger.error(`[Tor error] ${msg}`);
+ break;
+ case "WARN":
+ logger.warn(`[Tor warning] ${msg}`);
+ break;
+ default:
+ logger.info(`[Tor ${type.toLowerCase()}] ${msg}`);
+ }
+ }
+
+ /**
+ * Handle a notification that a new circuit has been built.
+ * If a change of bridge is detected (including a change from bridge to a
+ * normal guard), a notification is broadcast.
+ *
+ * @param {CircuitID} id The circuit ID
+ * @param {NodeFingerprint[]} nodes The nodes that compose the circuit
+ */
+ async onCircuitBuilt(id, nodes) {
+ this.#circuits.set(id, nodes);
+ logger.debug(`Built tor circuit ${id}`, 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;
+ }
+
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Handle a notification of a circuit being closed. We use it to clean the
+ * internal data.
+ *
+ * @param {CircuitID} id The circuit id
+ */
+ onCircuitClosed(id) {
+ logger.debug("Circuit closed event", id);
+ this.#circuits.delete(id);
+ }
+
+ /**
+ * Handle a notification about a stream switching to the sentconnect status.
+ *
+ * @param {StreamID} streamId The ID of the stream that switched to the
+ * sentconnect status.
+ * @param {CircuitID} circuitId The ID of the circuit used by the stream
+ * @param {string} username The SOCKS username
+ * @param {string} password The SOCKS password
+ */
+ async onStreamSentConnect(streamId, circuitId, username, password) {
+ if (!username || !password) {
+ return;
+ }
+ logger.debug("Stream sentconnect event", username, password, circuitId);
+ let circuit = this.#circuits.get(circuitId);
+ if (!circuit) {
+ circuit = new Promise((resolve, reject) => {
+ this.#controlConnection.getCircuits().then(circuits => {
+ for (const { id, nodes } of circuits) {
+ if (id === circuitId) {
+ resolve(nodes);
+ return;
+ }
+ // Opportunistically collect circuits, since we are iterating them.
+ this.#circuits.set(id, nodes);
+ }
+ logger.error(
+ `Seen a STREAM SENTCONNECT with circuit ${circuitId}, but Tor did not send information about it.`
+ );
+ reject();
+ });
+ });
+ this.#circuits.set(circuitId, circuit);
+ }
+ try {
+ circuit = await circuit;
+ } catch {
+ return;
+ }
+ Services.obs.notifyObservers(
+ {
+ wrappedJSObject: {
+ username,
+ password,
+ circuit,
+ },
+ },
+ TorProviderTopics.CircuitCredentialsMatched
+ );
+ }
+}
diff --git a/toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs b/toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs
@@ -0,0 +1,338 @@
+/* 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, {
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ TorProvider: "resource://gre/modules/TorProvider.sys.mjs",
+});
+
+export const TorProviderTopics = Object.freeze({
+ ProcessExited: "TorProcessExited",
+ BootstrapStatus: "TorBootstrapStatus",
+ BootstrapError: "TorBootstrapError",
+ TorLog: "TorLog",
+ HasWarnOrErr: "TorLogHasWarnOrErr",
+ BridgeChanged: "TorBridgeChanged",
+ CircuitCredentialsMatched: "TorCircuitCredentialsMatched",
+});
+
+/**
+ * Wrapper error class for errors raised during TorProvider.init.
+ */
+export class TorProviderInitError extends Error {
+ /**
+ * Create a new instance.
+ *
+ * @param {any} error - The raised error that we want to wrap.
+ */
+ constructor(error) {
+ super(error?.message, { cause: error });
+ this.name = "TorProviderInitError";
+ }
+}
+
+/**
+ * Bootstrap errors raised by the TorProvider.
+ */
+export class TorBootstrapError extends Error {
+ /**
+ * Create a new instance.
+ *
+ * @param {object} details - Details about the error.
+ * @param {string} details.summary - A summary of the error.
+ * @param {string} details.phase - The bootstrap phase when the error occured.
+ * @param {string} details.reason - The reason for the bootsrap failure.
+ */
+ constructor(details) {
+ super(details.summary);
+ this.name = "TorBootstrapError";
+ this.phase = details.phase;
+ this.reason = details.reason;
+ }
+}
+
+export const TorProviders = Object.freeze({
+ none: 0,
+ tor: 1,
+});
+
+/**
+ * @typedef {object} LogEntry An object with a log message
+ * @property {string} timestamp The local date-time stamp at which we received the message
+ * @property {string} type The message level
+ * @property {string} msg The message
+ */
+
+/**
+ * The factory to get a Tor provider.
+ * Currently we support only TorProvider, i.e., the one that interacts with
+ * C-tor through the control port protocol.
+ */
+export class TorProviderBuilder {
+ /**
+ * A promise with the instance of the provider that we are using.
+ *
+ * @type {Promise<TorProvider>?}
+ */
+ static #provider = null;
+
+ /**
+ * A record of the log messages from all TorProvider instances.
+ *
+ * @type {LogEntry[]}
+ */
+ static #log = [];
+
+ /**
+ * Get a record of historic log entries.
+ *
+ * @returns {LogEntry[]} - The record of entries.
+ */
+ static getLog() {
+ return structuredClone(this.#log);
+ }
+
+ /**
+ * The limit on the number of log entries we should store.
+ *
+ * @type {integer}
+ */
+ static #logLimit;
+
+ /**
+ * The observer that checks for new TorLog messages.
+ *
+ * @type {Function}
+ */
+ static #logObserver;
+
+ /**
+ * Add a new log message.
+ *
+ * @param {LogEntry} logEntry - The log entry to add.
+ */
+ static #addLogEntry(logEntry) {
+ if (this.#logLimit > 0 && this.#log.length >= this.#logLimit) {
+ this.#log.splice(0, 1);
+ }
+ this.#log.push(logEntry);
+ }
+
+ /**
+ * The observer that checks when the tor process exits, and reinitializes the
+ * provider.
+ *
+ * @type {Function}
+ */
+ static #exitObserver = null;
+
+ /**
+ * Tell whether the browser UI is ready.
+ * We ignore any errors until it is because we cannot show them.
+ *
+ * @type {boolean}
+ */
+ static #uiReady = false;
+
+ /**
+ * Initialize the provider of choice.
+ */
+ static init() {
+ this.#logLimit = Services.prefs.getIntPref(
+ "extensions.torlauncher.max_tor_log_entries",
+ 1000
+ );
+ this.#logObserver = subject => {
+ this.#addLogEntry(subject.wrappedJSObject);
+ };
+ Services.obs.addObserver(this.#logObserver, TorProviderTopics.TorLog);
+
+ switch (this.providerType) {
+ case TorProviders.tor:
+ // Even though initialization of the initial TorProvider is
+ // asynchronous, we do not expect the caller to await it. The reason is
+ // that any call to build() will wait the initialization anyway (and
+ // re-throw any initialization error).
+ this.#initTorProvider();
+ break;
+ case TorProviders.none:
+ lazy.TorLauncherUtil.setProxyConfiguration(
+ lazy.TorLauncherUtil.getPreferredSocksConfiguration()
+ );
+ break;
+ default:
+ console.error(`Unknown tor provider ${this.providerType}.`);
+ break;
+ }
+ }
+
+ /**
+ * Replace #provider with a new instance.
+ *
+ * @returns {Promise<TorProvider>} The new instance.
+ */
+ static #initTorProvider() {
+ if (!this.#exitObserver) {
+ this.#exitObserver = this.#torExited.bind(this);
+ Services.obs.addObserver(
+ this.#exitObserver,
+ TorProviderTopics.ProcessExited
+ );
+ }
+
+ // NOTE: We need to ensure that the #provider is set as soon
+ // TorProviderBuilder.init is called.
+ // I.e. it should be safe to call
+ // TorProviderBuilder.init();
+ // TorProviderBuilder.build();
+ // without any await.
+ //
+ // Therefore, we await the oldProvider within the Promise rather than make
+ // #initTorProvider async.
+ //
+ // In particular, this is needed by TorConnect when the user has selected
+ // quickstart, in which case `TorConnect.init` will immediately request the
+ // provider. See tor-browser#41921.
+ this.#provider = this.#replaceTorProvider(this.#provider);
+ return this.#provider;
+ }
+
+ /**
+ * Replace a TorProvider instance. Resolves once the TorProvider is
+ * initialised.
+ *
+ * @param {Promise<TorProvider>?} oldProvider - The previous's provider's
+ * promise, if any.
+ * @returns {TorProvider} The new TorProvider instance.
+ */
+ static async #replaceTorProvider(oldProvider) {
+ try {
+ // Uninitialise the old TorProvider, if there is any.
+ (await oldProvider)?.uninit();
+ } catch {}
+ const provider = new lazy.TorProvider();
+ try {
+ await provider.init();
+ } catch (error) {
+ // Wrap in an error type for callers to know whether the error comes from
+ // initialisation or something else.
+ throw new TorProviderInitError(error);
+ }
+ return provider;
+ }
+
+ static uninit() {
+ this.#provider?.then(provider => {
+ provider.uninit();
+ this.#provider = null;
+ });
+ if (this.#exitObserver) {
+ Services.obs.removeObserver(
+ this.#exitObserver,
+ TorProviderTopics.ProcessExited
+ );
+ this.#exitObserver = null;
+ }
+ Services.obs.removeObserver(this.#logObserver, TorProviderTopics.TorLog);
+ }
+
+ /**
+ * Build a provider.
+ * This method will wait for the system to be initialized, and allows you to
+ * catch also any initialization errors.
+ *
+ * @returns {TorProvider} A TorProvider instance
+ */
+ static async build() {
+ if (!this.#provider && this.providerType === TorProviders.none) {
+ throw new Error(
+ "Tor Browser has been configured to use only the proxy functionalities."
+ );
+ } else if (!this.#provider) {
+ throw new Error(
+ "The provider has not been initialized or already uninitialized."
+ );
+ }
+ return this.#provider;
+ }
+
+ /**
+ * Check if the provider has been succesfully initialized when the first
+ * browser window is shown.
+ * This is a workaround we need because ideally we would like the tor process
+ * to start as soon as possible, to avoid delays in the about:torconnect page,
+ * but we should modify TorConnect and about:torconnect to handle this case
+ * there with a better UX.
+ */
+ static async firstWindowLoaded() {
+ // FIXME: Just integrate this with the about:torconnect or about:tor UI.
+ if (
+ !lazy.TorLauncherUtil.shouldStartAndOwnTor ||
+ this.providerType !== TorProviders.tor
+ ) {
+ // If we are not managing the Tor daemon we cannot restart it, so just
+ // early return.
+ return;
+ }
+ let running = false;
+ try {
+ const provider = await this.#provider;
+ // The initialization might have succeeded, but so far we have ignored any
+ // error notification. So, check that the process has not exited after the
+ // provider has been initialized successfully, but the UI was not ready
+ // yet.
+ running = provider.isRunning;
+ } catch {
+ // Not even initialized, running is already false.
+ }
+ while (!running && lazy.TorLauncherUtil.showRestartPrompt(true)) {
+ try {
+ await this.#initTorProvider();
+ running = true;
+ } catch {}
+ }
+ // The user might have canceled the restart, but at this point the UI is
+ // ready in any case.
+ this.#uiReady = true;
+ }
+
+ static async #torExited() {
+ if (!this.#uiReady) {
+ console.warn(
+ `Seen ${TorProviderTopics.ProcessExited}, but not doing anything because the UI is not ready yet.`
+ );
+ return;
+ }
+ while (lazy.TorLauncherUtil.showRestartPrompt(false)) {
+ try {
+ await this.#initTorProvider();
+ break;
+ } catch {}
+ }
+ }
+
+ /**
+ * Return the provider chosen by the user.
+ * This function checks the TOR_PROVIDER environment variable and if it is a
+ * known provider, it returns its associated value.
+ * Otherwise, if it is not valid, the C tor implementation is chosen as the
+ * default one.
+ *
+ * @returns {number} An entry from TorProviders
+ */
+ static get providerType() {
+ // TODO: Add a preference to permanently save this without and avoid always
+ // using an environment variable.
+ let provider = TorProviders.tor;
+ const kEnvName = "TOR_PROVIDER";
+ if (
+ Services.env.exists(kEnvName) &&
+ Services.env.get(kEnvName) in TorProviders
+ ) {
+ provider = TorProviders[Services.env.get(kEnvName)];
+ }
+ return provider;
+ }
+}
diff --git a/toolkit/components/tor-launcher/TorStartupService.sys.mjs b/toolkit/components/tor-launcher/TorStartupService.sys.mjs
@@ -0,0 +1,50 @@
+const lazy = {};
+
+// We will use the modules only when the profile is loaded, so prefer lazy
+// loading
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs",
+ TorProviderBuilder: "resource://gre/modules/TorProviderBuilder.sys.mjs",
+});
+
+/* Browser observer topis */
+const BrowserTopics = Object.freeze({
+ ProfileAfterChange: "profile-after-change",
+ QuitApplicationGranted: "quit-application-granted",
+});
+
+let gInited = false;
+
+/**
+ * This class is registered as an observer, and will be instanced automatically
+ * by Firefox.
+ * When it observes profile-after-change, it initializes whatever is needed to
+ * launch Tor.
+ */
+export class TorStartupService {
+ observe(aSubject, aTopic) {
+ if (aTopic === BrowserTopics.ProfileAfterChange && !gInited) {
+ this.#init();
+ } else if (aTopic === BrowserTopics.QuitApplicationGranted) {
+ this.#uninit();
+ }
+ }
+
+ #init() {
+ Services.obs.addObserver(this, BrowserTopics.QuitApplicationGranted);
+
+ // Theoretically, build() is expected to await the initialization of the
+ // provider, and anything needing the Tor Provider should be able to just
+ // await on TorProviderBuilder.build().
+ lazy.TorProviderBuilder.init();
+
+ gInited = true;
+ }
+
+ #uninit() {
+ Services.obs.removeObserver(this, BrowserTopics.QuitApplicationGranted);
+
+ lazy.TorProviderBuilder.uninit();
+ lazy.TorLauncherUtil.cleanupTempDirectories();
+ }
+}
diff --git a/toolkit/components/tor-launcher/components.conf b/toolkit/components/tor-launcher/components.conf
@@ -0,0 +1,10 @@
+Classes = [
+ {
+ "cid": "{df46c65d-be2b-4d16-b280-69733329eecf}",
+ "contract_ids": [
+ "@torproject.org/tor-startup-service;1"
+ ],
+ "esModule": "resource://gre/modules/TorStartupService.sys.mjs",
+ "constructor": "TorStartupService",
+ },
+]
diff --git a/toolkit/components/tor-launcher/moz.build b/toolkit/components/tor-launcher/moz.build
@@ -0,0 +1,19 @@
+EXTRA_JS_MODULES += [
+ "TorBootstrapRequest.sys.mjs",
+ "TorControlPort.sys.mjs",
+ "TorLauncherUtil.sys.mjs",
+ "TorParsers.sys.mjs",
+ "TorProcess.sys.mjs",
+ "TorProcessAndroid.sys.mjs",
+ "TorProvider.sys.mjs",
+ "TorProviderBuilder.sys.mjs",
+ "TorStartupService.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXTRA_COMPONENTS += [
+ "tor-launcher.manifest",
+]
diff --git a/toolkit/components/tor-launcher/tor-launcher.manifest b/toolkit/components/tor-launcher/tor-launcher.manifest
@@ -0,0 +1,2 @@
+category profile-after-change TorStartupService @torproject.org/tor-startup-service;1
+category browser-first-window-ready resource://gre/modules/TorProviderBuilder.sys.mjs TorProviderBuilder.firstWindowLoaded