[tor-commits] [Git][tpo/applications/tor-browser][tor-browser-115.1.0esr-13.0-1] 2 commits: squash! Bug 40933: Add tor-launcher functionality
Pier Angelo Vendrame (@pierov)
git at gitlab.torproject.org
Fri Aug 4 18:03:02 UTC 2023
Pier Angelo Vendrame pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
57b25177 by Pier Angelo Vendrame at 2023-08-04T20:02:03+02:00
squash! Bug 40933: Add tor-launcher functionality
Bug 41926: Reimplement the control port
- - - - -
9722ca26 by Pier Angelo Vendrame at 2023-08-04T20:02:04+02:00
fixup! Bug 10760: Integrate TorButton to TorBrowser core
Removed torbutton.js, tor-control-port.js and utils.js.
- - - - -
13 changed files:
- browser/base/content/browser.xhtml
- + toolkit/components/tor-launcher/TorControlPort.sys.mjs
- toolkit/components/tor-launcher/TorMonitorService.sys.mjs
- toolkit/components/tor-launcher/TorProtocolService.sys.mjs
- toolkit/components/tor-launcher/moz.build
- − toolkit/torbutton/chrome/content/torbutton.js
- − toolkit/torbutton/components.conf
- toolkit/torbutton/jar.mn
- − toolkit/torbutton/modules/TorbuttonLogger.jsm
- − toolkit/torbutton/modules/tor-control-port.js
- − toolkit/torbutton/modules/utils.js
- toolkit/torbutton/moz.build
- tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
Changes:
=====================================
browser/base/content/browser.xhtml
=====================================
@@ -130,17 +130,11 @@
Services.scriptloader.loadSubScript("chrome://browser/content/search/autocomplete-popup.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/search/searchbar.js", this);
Services.scriptloader.loadSubScript("chrome://browser/content/languageNotification.js", this);
- Services.scriptloader.loadSubScript("chrome://torbutton/content/torbutton.js", this);
window.onload = gBrowserInit.onLoad.bind(gBrowserInit);
window.onunload = gBrowserInit.onUnload.bind(gBrowserInit);
window.onclose = WindowIsClosing;
- //onLoad Handler
- try {
- window.addEventListener("load", torbutton_init);
- } catch (e) {}
-
window.addEventListener("MozBeforeInitialXULLayout",
gBrowserInit.onBeforeInitialXULLayout.bind(gBrowserInit), { once: true });
=====================================
toolkit/components/tor-launcher/TorControlPort.sys.mjs
=====================================
@@ -0,0 +1,1534 @@
+import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs";
+
+/**
+ * @callback MessageCallback A callback to receive messages from the control
+ * port.
+ * @param {string} message The message to handle
+ */
+/**
+ * @callback RemoveCallback A function used to remove a previously registered
+ * callback.
+ */
+
+class CallbackDispatcher {
+ #callbackPairs = [];
+
+ /**
+ * Register a callback to handle a certain type of responses.
+ *
+ * @param {RegExp} regex The regex that tells which messages the callback
+ * wants to handle.
+ * @param {MessageCallback} callback The function to call
+ * @returns {RemoveCallback} A function to remove the just added callback
+ */
+ addCallback(regex, callback) {
+ this.#callbackPairs.push([regex, callback]);
+ }
+
+ /**
+ * Push a certain message to all the callbacks whose regex matches it.
+ *
+ * @param {string} message The message to push to the callbacks
+ */
+ pushMessage(message) {
+ for (const [regex, callback] of this.#callbackPairs) {
+ if (message.match(regex)) {
+ callback(message);
+ }
+ }
+ }
+}
+
+/**
+ * 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.
+ */
+ 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.
+ */
+ 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 shoulw 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) => {
+ // asyncWait next write request
+ 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();
+
+ // request next wait if there is one
+ tryAsyncWait();
+
+ // finally resolve promise
+ resolve(bytesWritten);
+ } catch (err) {
+ // reject 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: stream => {
+ 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
+ */
+
+class TorError extends Error {
+ constructor(command, reply) {
+ super(`${command} -> ${reply}`);
+ this.name = "TorError";
+ const info = reply.match(/(?<code>\d{3})(?:\s(?<message>.+))?/);
+ this.torStatusCode = info.groups.code;
+ if (info.groups.message) {
+ this.torMessage = info.groups.message;
+ }
+ }
+}
+
+class ControlSocket {
+ /**
+ * The socket to write to the control port.
+ *
+ * @type {AsyncSocket}
+ */
+ #socket;
+
+ /**
+ * The dispatcher used for the data we receive over the control port.
+ *
+ * @type {CallbackDispatcher}
+ */
+ #mainDispatcher = new CallbackDispatcher();
+ /**
+ * A secondary dispatcher used only to dispatch aynchronous events.
+ *
+ * @type {CallbackDispatcher}
+ */
+ #notificationDispatcher = new CallbackDispatcher();
+
+ /**
+ * 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.
+ *
+ * @type {Command[]}
+ */
+ #commandQueue = [];
+
+ constructor(asyncSocket) {
+ this.#socket = asyncSocket;
+
+ // #mainDispatcher pushes only async notifications (650) to
+ // #notificationDispatcher
+ this.#mainDispatcher.addCallback(
+ /^650/,
+ this.#handleNotification.bind(this)
+ );
+ // callback for handling responses and errors
+ this.#mainDispatcher.addCallback(
+ /^[245]\d\d/,
+ this.#handleCommandReply.bind(this)
+ );
+
+ this.#startMessagePump();
+ }
+
+ /**
+ * 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");
+ }
+
+ /**
+ * 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();
+ // log("controlPort >> " + message);
+ this.#mainDispatcher.pushMessage(message);
+ }
+ } catch (err) {
+ try {
+ this.#close(err);
+ } catch (ec) {
+ console.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];
+ // log("controlPort << " + cmd.commandString);
+ 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
+ * @returns {Promise<string>} The message sent by the control port. It will
+ * always start with 2xx. In case of other codes the function will throw,
+ * instead. This means that the return value will 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();
+ }
+ });
+ }
+
+ /**
+ * Handles a message starting with 2xx, 4xx, or 5xx.
+ * This function should be used only as a callback for the main dispatcher.
+ *
+ * @param {string} message The message to handle
+ */
+ #handleCommandReply(message) {
+ const cmd = this.#commandQueue.shift();
+ if (message[0] === "2") {
+ cmd.resolve(message);
+ } else if (message.match(/^[45]/)) {
+ cmd.reject(new TorError(cmd.commandString, message));
+ } else {
+ // This should never happen, as the dispatcher should filter the messages
+ // already.
+ cmd.reject(
+ new Error(`Received unexpected message:\n----\n${message}\n----`)
+ );
+ }
+
+ // send next command if one is available
+ if (this.#commandQueue.length) {
+ this.#writeNextCommand();
+ }
+ }
+
+ /**
+ * Re-route an event message to the notification dispatcher.
+ * This function should be used only as a callback for the main dispatcher.
+ *
+ * @param {string} message The message received on the control port
+ */
+ #handleNotification(message) {
+ try {
+ this.#notificationDispatcher.pushMessage(message);
+ } catch (e) {
+ console.error("An event watcher threw", e);
+ }
+ }
+
+ /**
+ * 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) {
+ 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);
+ }
+
+ /**
+ * Register an event watcher.
+ *
+ * @param {RegExp} regex The regex to filter on messages to receive
+ * @param {MessageCallback} callback The callback for the messages
+ */
+ addNotificationCallback(regex, callback) {
+ this.#notificationDispatcher.addCallback(regex, callback);
+ }
+
+ /**
+ * Tells whether the underlying socket is still open.
+ */
+ get isOpen() {
+ return !!this.#socket;
+ }
+}
+
+// ## utils
+// A namespace for utility functions
+let utils = {};
+
+// __utils.identity(x)__.
+// Returns its argument unchanged.
+utils.identity = function (x) {
+ return x;
+};
+
+// __utils.capture(string, regex)__.
+// Takes a string and returns an array of capture items, where regex must have a single
+// capturing group and use the suffix /.../g to specify a global search.
+utils.capture = function (string, regex) {
+ let matches = [];
+ // Special trick to use string.replace for capturing multiple matches.
+ string.replace(regex, function (a, captured) {
+ matches.push(captured);
+ });
+ return matches;
+};
+
+// __utils.extractor(regex)__.
+// Returns a function that takes a string and returns an array of regex matches. The
+// regex must use the suffix /.../g to specify a global search.
+utils.extractor = function (regex) {
+ return function (text) {
+ return utils.capture(text, regex);
+ };
+};
+
+// __utils.splitLines(string)__.
+// Splits a string into an array of strings, each corresponding to a line.
+utils.splitLines = function (string) {
+ return string.split(/\r?\n/);
+};
+
+// __utils.splitAtSpaces(string)__.
+// Splits a string into chunks between spaces. Does not split at spaces
+// inside pairs of quotation marks.
+utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
+
+// __utils.splitAtFirst(string, regex)__.
+// Splits a string at the first instance of regex match. If no match is
+// found, returns the whole string.
+utils.splitAtFirst = function (string, regex) {
+ let match = string.match(regex);
+ return match
+ ? [
+ string.substring(0, match.index),
+ string.substring(match.index + match[0].length),
+ ]
+ : string;
+};
+
+// __utils.splitAtEquals(string)__.
+// Splits a string into chunks between equals. Does not split at equals
+// inside pairs of quotation marks.
+utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
+
+// __utils.mergeObjects(arrayOfObjects)__.
+// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
+// Pure function.
+utils.mergeObjects = function (arrayOfObjects) {
+ let result = {};
+ for (let obj of arrayOfObjects) {
+ for (let key in obj) {
+ result[key] = obj[key];
+ }
+ }
+ return result;
+};
+
+// __utils.listMapData(parameterString, listNames)__.
+// Takes a list of parameters separated by spaces, of which the first several are
+// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
+// to the unnamed parameters, and combine them in a map with the named parameters.
+// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
+//
+// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
+// ["streamID", "event", "circuitID", "IP"])
+// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
+// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
+utils.listMapData = function (parameterString, listNames) {
+ // Split out the space-delimited parameters.
+ let parameters = utils.splitAtSpaces(parameterString),
+ dataMap = {};
+ // Assign listNames to the first n = listNames.length parameters.
+ for (let i = 0; i < listNames.length; ++i) {
+ dataMap[listNames[i]] = parameters[i];
+ }
+ // Read key-value pairs and copy these to the dataMap.
+ for (let i = listNames.length; i < parameters.length; ++i) {
+ let [key, value] = utils.splitAtEquals(parameters[i]);
+ if (key && value) {
+ dataMap[key] = value;
+ }
+ }
+ return dataMap;
+};
+
+// ## info
+// A namespace for functions related to tor's GETINFO and GETCONF command.
+let info = {};
+
+// __info.keyValueStringsFromMessage(messageText)__.
+// Takes a message (text) response to GETINFO or GETCONF and provides
+// a series of key-value strings, which are either multiline (with a `250+` prefix):
+//
+// 250+config/defaults=
+// AccountingMax "0 bytes"
+// AllowDotExit "0"
+// .
+//
+// or single-line (with a `250-` or `250 ` prefix):
+//
+// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
+info.keyValueStringsFromMessage = utils.extractor(
+ /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
+);
+
+// __info.applyPerLine(transformFunction)__.
+// Returns a function that splits text into lines,
+// and applies transformFunction to each line.
+info.applyPerLine = function (transformFunction) {
+ return function (text) {
+ return utils.splitLines(text.trim()).map(transformFunction);
+ };
+};
+
+// __info.routerStatusParser(valueString)__.
+// Parses a router status entry as, described in
+// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
+// (search for "router status entry")
+info.routerStatusParser = function (valueString) {
+ let lines = utils.splitLines(valueString),
+ objects = [];
+ for (let line of lines) {
+ // Drop first character and grab data following it.
+ let myData = line.substring(2),
+ // Accumulate more maps with data, depending on the first character in the line.
+ dataFun = {
+ r: data =>
+ utils.listMapData(data, [
+ "nickname",
+ "identity",
+ "digest",
+ "publicationDate",
+ "publicationTime",
+ "IP",
+ "ORPort",
+ "DirPort",
+ ]),
+ a: data => ({ IPv6: data }),
+ s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
+ v: data => ({ version: data }),
+ w: data => utils.listMapData(data, []),
+ p: data => ({ portList: data.split(",") }),
+ }[line.charAt(0)];
+ if (dataFun !== undefined) {
+ objects.push(dataFun(myData));
+ }
+ }
+ return utils.mergeObjects(objects);
+};
+
+// __info.circuitStatusParser(line)__.
+// Parse the output of a circuit status line.
+info.circuitStatusParser = function (line) {
+ let data = utils.listMapData(line, ["id", "status", "circuit"]),
+ circuit = data.circuit;
+ // Parse out the individual circuit IDs and names.
+ if (circuit) {
+ data.circuit = circuit.split(",").map(function (x) {
+ return x.split(/~|=/);
+ });
+ }
+ return data;
+};
+
+// __info.streamStatusParser(line)__.
+// Parse the output of a stream status line.
+info.streamStatusParser = function (text) {
+ return utils.listMapData(text, [
+ "StreamID",
+ "StreamStatus",
+ "CircuitID",
+ "Target",
+ ]);
+};
+
+// TODO: fix this parsing logic to handle bridgeLine correctly
+// fingerprint/id is an optional parameter
+// __info.bridgeParser(bridgeLine)__.
+// Takes a single line from a `getconf bridge` result and returns
+// a map containing the bridge's type, address, and ID.
+info.bridgeParser = function (bridgeLine) {
+ let result = {},
+ tokens = bridgeLine.split(/\s+/);
+ // First check if we have a "vanilla" bridge:
+ if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
+ result.type = "vanilla";
+ [result.address, result.ID] = tokens;
+ // Several bridge types have a similar format:
+ } else {
+ result.type = tokens[0];
+ if (
+ [
+ "flashproxy",
+ "fte",
+ "meek",
+ "meek_lite",
+ "obfs3",
+ "obfs4",
+ "scramblesuit",
+ "snowflake",
+ ].includes(result.type)
+ ) {
+ [result.address, result.ID] = tokens.slice(1);
+ }
+ }
+ return result.type ? result : null;
+};
+
+// __info.parsers__.
+// A map of GETINFO and GETCONF keys to parsing function, which convert
+// result strings to JavaScript data.
+info.parsers = {
+ "ns/id/": info.routerStatusParser,
+ "ip-to-country/": utils.identity,
+ "circuit-status": info.applyPerLine(info.circuitStatusParser),
+ bridge: info.bridgeParser,
+ // Currently unused parsers:
+ // "ns/name/" : info.routerStatusParser,
+ // "stream-status" : info.applyPerLine(info.streamStatusParser),
+ // "version" : utils.identity,
+ // "config-file" : utils.identity,
+};
+
+// __info.getParser(key)__.
+// Takes a key and determines the parser function that should be used to
+// convert its corresponding valueString to JavaScript data.
+info.getParser = function (key) {
+ return (
+ info.parsers[key] ||
+ info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
+ );
+};
+
+// __info.stringToValue(string)__.
+// Converts a key-value string as from GETINFO or GETCONF to a value.
+info.stringToValue = function (string) {
+ // key should look something like `250+circuit-status=` or `250-circuit-status=...`
+ // or `250 circuit-status=...`
+ let matchForKey = string.match(/^250[ +-](.+?)=/),
+ key = matchForKey ? matchForKey[1] : null;
+ if (key === null) {
+ return null;
+ }
+ // matchResult finds a single-line result for `250-` or `250 `,
+ // or a multi-line one for `250+`.
+ let matchResult =
+ string.match(/^250[ -].+?=(.*)$/) ||
+ string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
+ // Retrieve the captured group (the text of the value in the key-value pair)
+ valueString = matchResult ? matchResult[1] : null,
+ // Get the parser function for the key found.
+ parse = info.getParser(key.toLowerCase());
+ if (parse === undefined) {
+ throw new Error("No parser found for '" + key + "'");
+ }
+ // Return value produced by the parser.
+ return parse(valueString);
+};
+
+/**
+ * @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 {string} 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} 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 EventFilterCallback
+ * @param {any} data Either a raw string, or already parsed data
+ * @returns {boolean}
+ */
+/**
+ * @callback EventCallback
+ * @param {any} data Either a raw string, or already parsed data
+ */
+
+class TorController {
+ /**
+ * The control socket
+ *
+ * @type {ControlSocket}
+ */
+ #socket;
+
+ /**
+ * A map of EVENT keys to parsing functions, which convert result strings to
+ * JavaScript data.
+ */
+ #eventParsers = {
+ stream: info.streamStatusParser,
+ // Currently unused:
+ // "circ" : info.circuitStatusParser,
+ };
+
+ /**
+ * Builds a new TorController.
+ *
+ * @param {AsyncSocket} socket The socket to communicate to the control port
+ */
+ constructor(socket) {
+ this.#socket = new ControlSocket(socket);
+ }
+
+ /**
+ * Tells whether the underlying socket is open.
+ *
+ * @returns {boolean}
+ */
+ get isOpen() {
+ return this.#socket.isOpen;
+ }
+
+ /**
+ * Close the underlying socket.
+ */
+ close() {
+ this.#socket.close();
+ }
+
+ /**
+ * Send a command over the control port.
+ * TODO: Make this function private, and force the operations to go through
+ * specialized methods.
+ *
+ * @param {string} cmd The command to send
+ * @returns {Promise<string>} A 2xx response obtained from the control port.
+ * For other codes, this function will throw. The returned string will never
+ * be empty.
+ */
+ async sendCommand(cmd) {
+ return this.#socket.sendCommand(cmd);
+ }
+
+ /**
+ * 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 TorError(command, reply);
+ }
+ }
+
+ /**
+ * Authenticate to the tor daemon.
+ * Notice that a failure in the authentication makes the connection close.
+ *
+ * @param {string} password The password for the control port.
+ */
+ async authenticate(password) {
+ if (password) {
+ this.#expectString(password, "password");
+ }
+ await this.#sendCommandSimple(`authenticate ${password || ""}`);
+ }
+
+ /**
+ * Sends a GETINFO for a single key.
+ *
+ * @param {string} key The key to get value for
+ * @returns {any} The return value depends on the requested key
+ */
+ async getInfo(key) {
+ this.#expectString(key, "key");
+ const response = await this.sendCommand(`getinfo ${key}`);
+ return this.#getMultipleResponseValues(response)[0];
+ }
+
+ /**
+ * 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\+([^=]+)=([\s\S]*?)^\.\r?\n^250 OK\s*$/m);
+ if (!match || match[1] !== key) {
+ throw new TorError(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
+ // TODO: The consumers do not care about the port or the square brackets
+ // either. Remove them when integrating this function with the rest
+ const v6 = reply.match(/^a\s+(\[[0-9a-fA-F:]+\]:[0-9]{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<string[]>} 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 =>
+ TorParsers.unescapeString(m[1])
+ );
+ }
+
+ // Configuration
+
+ /**
+ * Sends a GETCONF for a single key.
+ * GETCONF with a single argument returns results with one or more lines that
+ * look like `250[- ]key=value`.
+ * Any GETCONF lines that contain a single keyword only are currently dropped.
+ * So we can use similar parsing to that for getInfo.
+ *
+ * @param {string} key The key to get value for
+ * @returns {any} A parsed config value (it depends if a parser is known)
+ */
+ async getConf(key) {
+ this.#expectString(key, "key");
+ return this.#getMultipleResponseValues(
+ await this.sendCommand(`getconf ${key}`)
+ );
+ }
+
+ /**
+ * 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 TorError(cmd, reply);
+ }
+ return values;
+ }
+
+ /**
+ * Get the bridges Tor has been configured with.
+ *
+ * @returns {Bridge[]} The configured bridges
+ */
+ async getBridges() {
+ return (await this.#getConf("BRIDGE")).map(TorParsers.parseBridgeLine);
+ }
+
+ /**
+ * 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 {object} values The values to set
+ */
+ async setConf(values) {
+ const args = Object.entries(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(" ");
+ return 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) {
+ return this.setConf({ DisableNetwork: !enabled });
+ }
+
+ /**
+ * Ask Tor to write out its config options into its torrc.
+ */
+ async flushSettings() {
+ return 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 TorError(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 = {
+ hsAddress: match.groups.HSAddress,
+ typeAndKey: `${match.groups.KeyType}:${match.groups.PrivateKeyBlob}`,
+ };
+ const maybeFlags = match.groups.other?.match(/Flags=(\S+)/);
+ if (maybeFlags) {
+ info.Flags = maybeFlags[1];
+ }
+ 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 TorError(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 TorError(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() {
+ return 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() {
+ return this.#sendCommandSimple("RESETCONF __OwningControllerProcess");
+ }
+
+ // Signals
+
+ /**
+ * Ask Tor to swtich to new circuits and clear the DNS cache.
+ */
+ async newnym() {
+ return 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.
+ */
+ 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(" "));
+ }
+
+ /**
+ * Watches for a particular type of asynchronous event.
+ * Notice: we only observe `"650" SP...` events, currently (no `650+...` or
+ * `650-...` events).
+ * Also, you need to enable the events in the control port with SETEVENTS,
+ * first.
+ *
+ * @param {string} type The event type to catch
+ * @param {EventFilterCallback?} filter An optional callback to filter
+ * events for which the callback will be called. If null, all events will be
+ * passed.
+ * @param {EventCallback} callback The callback that will handle the event
+ * @param {boolean} raw Tell whether to ignore the data parser, even if
+ * supported
+ */
+ watchEvent(type, filter, callback, raw = false) {
+ this.#expectString(type, "type");
+ const start = `650 ${type}`;
+ this.#socket.addNotificationCallback(new RegExp(`^${start}`), message => {
+ // Remove also the initial text
+ const dataText = message.substring(start.length + 1);
+ const parser = this.#eventParsers[type.toLowerCase()];
+ const data = dataText && parser ? parser(dataText) : null;
+ // FIXME: This is the original code, but we risk of not filtering on the
+ // data, if we ask for raw data (which we always do at the moment, but we
+ // do not use a filter either...)
+ if (filter === null || filter(data)) {
+ callback(data && !raw ? data : message);
+ }
+ });
+ }
+
+ // Other helpers
+
+ /**
+ * 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: 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;
+ }
+
+ /**
+ * 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.`);
+ }
+ }
+
+ /**
+ * 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])]
+ )
+ );
+ }
+
+ /**
+ * Process multiple responses to a GETINFO or GETCONF request.
+ *
+ * @param {string} message The message to process
+ * @returns {object[]} The keys depend on the message
+ */
+ #getMultipleResponseValues(message) {
+ return info
+ .keyValueStringsFromMessage(message)
+ .map(info.stringToValue)
+ .filter(x => x);
+ }
+}
+
+const controlPortInfo = {};
+
+/**
+ * Sets Tor control port connection parameters to be used in future calls to
+ * the controller() function.
+ *
+ * Example:
+ * configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
+ *
+ * @param {nsIFile?} ipcFile An optional file to use to communicate to the
+ * control port on Unix platforms
+ * @param {string?} host The hostname to connect to the control port. Mutually
+ * exclusive with ipcFile
+ * @param {integer?} port The port number of the control port. To be used only
+ * with host. The default is 9151.
+ * @param {string} password The password of the control port in clear text.
+ */
+export function configureControlPortModule(ipcFile, host, port, password) {
+ controlPortInfo.ipcFile = ipcFile;
+ controlPortInfo.host = host;
+ controlPortInfo.port = port || 9151;
+ controlPortInfo.password = password;
+}
+
+/**
+ * Instantiates and returns a controller object that is connected and
+ * authenticated to a Tor ControlPort using the connection parameters
+ * provided in the most recent call to configureControlPortModule().
+ *
+ * Example:
+ * // Get a new controller
+ * let c = await controller();
+ * // Send command and receive a `250` reply or an error message:
+ * let replyPromise = await c.getInfo("ip-to-country/16.16.16.16");
+ * // Close the controller permanently
+ * c.close();
+ */
+export async function controller() {
+ if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
+ throw new Error("Please call configureControlPortModule first");
+ }
+ let socket;
+ if (controlPortInfo.ipcFile) {
+ socket = AsyncSocket.fromIpcFile(controlPortInfo.ipcFile);
+ } else {
+ socket = AsyncSocket.fromSocketAddress(
+ controlPortInfo.host,
+ controlPortInfo.port
+ );
+ }
+ const controller = new TorController(socket);
+ try {
+ await controller.authenticate(controlPortInfo.password);
+ } catch (e) {
+ try {
+ controller.close();
+ } catch (ec) {
+ // TODO: Use a custom logger?
+ console.error("Cannot close the socket", ec);
+ }
+ throw e;
+ }
+ return controller;
+}
=====================================
toolkit/components/tor-launcher/TorMonitorService.sys.mjs
=====================================
@@ -15,14 +15,9 @@ const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
+ controller: "resource://gre/modules/TorControlPort.sys.mjs",
});
-ChromeUtils.defineModuleGetter(
- lazy,
- "controller",
- "resource://torbutton/modules/tor-control-port.js"
-);
-
ChromeUtils.defineESModuleGetters(lazy, {
TorProtocolService: "resource://gre/modules/TorProtocolService.sys.mjs",
});
@@ -172,9 +167,7 @@ export const TorMonitorService = {
const cmd = "GETINFO";
const key = "status/bootstrap-phase";
let reply = await this._connection.sendCommand(`${cmd} ${key}`);
- if (!reply) {
- throw new Error("We received an empty reply");
- }
+
// A typical reply looks like:
// 250-status/bootstrap-phase=NOTICE BOOTSTRAP PROGRESS=100 TAG=done SUMMARY="Done"
// 250 OK
@@ -335,8 +328,7 @@ export const TorMonitorService = {
let conn;
try {
- const avoidCache = true;
- conn = await lazy.controller(avoidCache);
+ conn = await lazy.controller();
} catch (e) {
logger.error("Cannot open a control port connection", e);
if (conn) {
@@ -353,12 +345,10 @@ export const TorMonitorService = {
}
// TODO: optionally monitor INFO and DEBUG log messages.
- let reply = await conn.sendCommand(
- "SETEVENTS " + Array.from(this._eventHandlers.keys()).join(" ")
- );
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- logger.error("SETEVENTS failed");
+ try {
+ await conn.setEvents(Array.from(this._eventHandlers.keys()));
+ } catch (e) {
+ logger.error("SETEVENTS failed", e);
conn.close();
return false;
}
@@ -405,18 +395,16 @@ export const TorMonitorService = {
// Try to become the primary controller (TAKEOWNERSHIP).
async _takeTorOwnership(conn) {
- const takeOwnership = "TAKEOWNERSHIP";
- let reply = await conn.sendCommand(takeOwnership);
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- logger.warn("Take ownership failed");
- } else {
- const resetConf = "RESETCONF __OwningControllerProcess";
- reply = await conn.sendCommand(resetConf);
- reply = TorParsers.parseCommandResponse(reply);
- if (!TorParsers.commandSucceeded(reply)) {
- logger.warn("Clear owning controller process failed");
- }
+ try {
+ conn.takeOwnership();
+ } catch (e) {
+ logger.warn("Take ownership failed", e);
+ return;
+ }
+ try {
+ conn.resetOwningControllerProcess();
+ } catch (e) {
+ logger.warn("Clear owning controller process failed", e);
}
},
=====================================
toolkit/components/tor-launcher/TorProtocolService.sys.mjs
=====================================
@@ -19,16 +19,10 @@ ChromeUtils.defineModuleGetter(
"TorMonitorService",
"resource://gre/modules/TorMonitorService.jsm"
);
-ChromeUtils.defineModuleGetter(
- lazy,
- "configureControlPortModule",
- "resource://torbutton/modules/tor-control-port.js"
-);
-ChromeUtils.defineModuleGetter(
- lazy,
- "controller",
- "resource://torbutton/modules/tor-control-port.js"
-);
+ChromeUtils.defineESModuleGetters(lazy, {
+ controller: "resource://gre/modules/TorControlPort.sys.mjs",
+ configureControlPortModule: "resource://gre/modules/TorControlPort.sys.mjs",
+});
const TorTopics = Object.freeze({
ProcessExited: "TorProcessExited",
@@ -285,8 +279,7 @@ export const TorProtocolService = {
});
},
- // TODO: transform the following 4 functions in getters. At the moment they
- // are also used in torbutton.
+ // TODO: transform the following 4 functions in getters.
// Returns Tor password string or null if an error occurs.
torGetPassword() {
@@ -490,8 +483,6 @@ export const TorProtocolService = {
TorLauncherUtil.setProxyConfiguration(this._SOCKSPortInfo);
// Set the global control port info parameters.
- // These values may be overwritten by torbutton when it initializes, but
- // torbutton's values *should* be identical.
lazy.configureControlPortModule(
this._controlIPCFile,
this._controlHost,
@@ -616,8 +607,7 @@ export const TorProtocolService = {
// return it.
async _getConnection() {
if (!this._controlConnection) {
- const avoidCache = true;
- this._controlConnection = await lazy.controller(avoidCache);
+ this._controlConnection = await lazy.controller();
}
if (this._controlConnection.inUse) {
await new Promise((resolve, reject) =>
=====================================
toolkit/components/tor-launcher/moz.build
=====================================
@@ -1,5 +1,6 @@
EXTRA_JS_MODULES += [
"TorBootstrapRequest.sys.mjs",
+ "TorControlPort.sys.mjs",
"TorDomainIsolator.sys.mjs",
"TorLauncherUtil.sys.mjs",
"TorMonitorService.sys.mjs",
=====================================
toolkit/torbutton/chrome/content/torbutton.js deleted
=====================================
@@ -1,148 +0,0 @@
-// window globals
-var torbutton_init;
-
-(() => {
- // Bug 1506 P1-P5: This is the main Torbutton overlay file. Much needs to be
- // preserved here, but in an ideal world, most of this code should perhaps be
- // moved into an XPCOM service, and much can also be tossed. See also
- // individual 1506 comments for details.
-
- // TODO: check for leaks: http://www.mozilla.org/scriptable/avoiding-leaks.html
- // TODO: Double-check there are no strange exploits to defeat:
- // http://kb.mozillazine.org/Links_to_local_pages_don%27t_work
-
- /* global gBrowser, Services, AppConstants */
-
- let { torbutton_log } = ChromeUtils.import(
- "resource://torbutton/modules/utils.js"
- );
- let { configureControlPortModule } = ChromeUtils.import(
- "resource://torbutton/modules/tor-control-port.js"
- );
-
- const { TorProtocolService } = ChromeUtils.import(
- "resource://gre/modules/TorProtocolService.jsm"
- );
-
- var m_tb_prefs = Services.prefs;
-
- // status
- var m_tb_wasinited = false;
-
- var m_tb_control_ipc_file = null; // Set if using IPC (UNIX domain socket).
- var m_tb_control_port = null; // Set if using TCP.
- var m_tb_control_host = null; // Set if using TCP.
- var m_tb_control_pass = null;
-
- // Bug 1506 P2-P4: This code sets some version variables that are irrelevant.
- // It does read out some important environment variables, though. It is
- // called once per browser window.. This might belong in a component.
- torbutton_init = function () {
- torbutton_log(3, "called init()");
-
- if (m_tb_wasinited) {
- return;
- }
- m_tb_wasinited = true;
-
- // Bug 1506 P4: These vars are very important for New Identity
- if (Services.env.exists("TOR_CONTROL_PASSWD")) {
- m_tb_control_pass = Services.env.get("TOR_CONTROL_PASSWD");
- } else if (Services.env.exists("TOR_CONTROL_COOKIE_AUTH_FILE")) {
- var cookie_path = Services.env.get("TOR_CONTROL_COOKIE_AUTH_FILE");
- try {
- if ("" != cookie_path) {
- m_tb_control_pass = torbutton_read_authentication_cookie(cookie_path);
- }
- } catch (e) {
- torbutton_log(4, "unable to read authentication cookie");
- }
- } else {
- try {
- // Try to get password from Tor Launcher.
- m_tb_control_pass = TorProtocolService.torGetPassword();
- } catch (e) {}
- }
-
- // Try to get the control port IPC file (an nsIFile) from Tor Launcher,
- // since Tor Launcher knows how to handle its own preferences and how to
- // resolve relative paths.
- try {
- m_tb_control_ipc_file = TorProtocolService.torGetControlIPCFile();
- } catch (e) {}
-
- if (!m_tb_control_ipc_file) {
- if (Services.env.exists("TOR_CONTROL_PORT")) {
- m_tb_control_port = Services.env.get("TOR_CONTROL_PORT");
- } else {
- try {
- const kTLControlPortPref = "extensions.torlauncher.control_port";
- m_tb_control_port = m_tb_prefs.getIntPref(kTLControlPortPref);
- } catch (e) {
- // Since we want to disable some features when Tor Launcher is
- // not installed (e.g., New Identity), we do not set a default
- // port value here.
- }
- }
-
- if (Services.env.exists("TOR_CONTROL_HOST")) {
- m_tb_control_host = Services.env.get("TOR_CONTROL_HOST");
- } else {
- try {
- const kTLControlHostPref = "extensions.torlauncher.control_host";
- m_tb_control_host = m_tb_prefs.getCharPref(kTLControlHostPref);
- } catch (e) {
- m_tb_control_host = "127.0.0.1";
- }
- }
- }
-
- configureControlPortModule(
- m_tb_control_ipc_file,
- m_tb_control_host,
- m_tb_control_port,
- m_tb_control_pass
- );
-
- torbutton_log(3, "init completed");
- };
-
- // Bug 1506 P4: Control port interaction. Needed for New Identity.
- function torbutton_read_authentication_cookie(path) {
- var file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.initWithPath(path);
- var fileStream = Cc[
- "@mozilla.org/network/file-input-stream;1"
- ].createInstance(Ci.nsIFileInputStream);
- fileStream.init(file, 1, 0, false);
- var binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
- Ci.nsIBinaryInputStream
- );
- binaryStream.setInputStream(fileStream);
- var array = binaryStream.readByteArray(fileStream.available());
- binaryStream.close();
- fileStream.close();
- return torbutton_array_to_hexdigits(array);
- }
-
- // Bug 1506 P4: Control port interaction. Needed for New Identity.
- function torbutton_array_to_hexdigits(array) {
- return array
- .map(function (c) {
- return String("0" + c.toString(16)).slice(-2);
- })
- .join("");
- }
-
- // ---------------------- Event handlers -----------------
-
- // Bug 1506 P3: This is needed pretty much only for the window resizing.
- // See comments for individual functions for details
- function torbutton_new_window(event) {
- torbutton_log(3, "New window");
- if (!m_tb_wasinited) {
- torbutton_init();
- }
- }
- window.addEventListener("load", torbutton_new_window);
-})();
=====================================
toolkit/torbutton/components.conf deleted
=====================================
@@ -1,10 +0,0 @@
-Classes = [
- {
- "cid": "{f36d72c9-9718-4134-b550-e109638331d7}",
- "contract_ids": [
- "@torproject.org/torbutton-logger;1"
- ],
- "jsm": "resource://torbutton/modules/TorbuttonLogger.jsm",
- "constructor": "TorbuttonLogger",
- },
-]
=====================================
toolkit/torbutton/jar.mn
=====================================
@@ -1,19 +1,12 @@
#filter substitution
torbutton.jar:
-
-% content torbutton %content/
-
- content/torbutton.js (chrome/content/torbutton.js)
-
- modules/ (modules/*)
-
% resource torbutton %
+% category l10n-registry torbutton resource://torbutton/locale/{locale}/
# browser branding
% override chrome://branding/locale/brand.dtd chrome://torbutton/locale/brand.dtd
% override chrome://branding/locale/brand.properties chrome://torbutton/locale/brand.properties
-% category l10n-registry torbutton resource://torbutton/locale/{locale}/
# Strings for the about:tbupdate page
% override chrome://browser/locale/aboutTBUpdate.dtd chrome://torbutton/locale/aboutTBUpdate.dtd
=====================================
toolkit/torbutton/modules/TorbuttonLogger.jsm deleted
=====================================
@@ -1,147 +0,0 @@
-// Bug 1506 P1: This is just a handy logger. If you have a better one, toss
-// this in the trash.
-
-/*************************************************************************
- * TBLogger (JavaScript XPCOM component)
- *
- * Allows loglevel-based logging to different logging mechanisms.
- *
- *************************************************************************/
-
-var EXPORTED_SYMBOLS = ["TorbuttonLogger"];
-
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-function TorbuttonLogger() {
- // Register observer
- Services.prefs.addObserver("extensions.torbutton", this);
-
- this.loglevel = Services.prefs.getIntPref("extensions.torbutton.loglevel", 4);
- this.logmethod = Services.prefs.getIntPref(
- "extensions.torbutton.logmethod",
- 1
- );
-
- try {
- var logMngr = Cc["@mozmonkey.com/debuglogger/manager;1"].getService(
- Ci.nsIDebugLoggerManager
- );
- this._debuglog = logMngr.registerLogger("torbutton");
- } catch (exErr) {
- this._debuglog = false;
- }
- this._console = Services.console;
-
- // This JSObject is exported directly to chrome
- this.wrappedJSObject = this;
- this.log(3, "Torbutton debug output ready");
-}
-
-/**
- * JS XPCOM component registration goop:
- *
- * Everything below is boring boilerplate and can probably be ignored.
- */
-
-TorbuttonLogger.prototype = {
- QueryInterface: ChromeUtils.generateQI([Ci.nsIClassInfo]),
-
- wrappedJSObject: null, // Initialized by constructor
-
- formatLog(str, level) {
- const padInt = n => String(n).padStart(2, "0");
- const logString = { 1: "VERB", 2: "DBUG", 3: "INFO", 4: "NOTE", 5: "WARN" };
- const d = new Date();
- const now =
- padInt(d.getUTCMonth() + 1) +
- "-" +
- padInt(d.getUTCDate()) +
- " " +
- padInt(d.getUTCHours()) +
- ":" +
- padInt(d.getUTCMinutes()) +
- ":" +
- padInt(d.getUTCSeconds());
- return `${now} Torbutton ${logString[level]}: ${str}`;
- },
-
- // error console log
- eclog(level, str) {
- switch (this.logmethod) {
- case 0: // stderr
- if (this.loglevel <= level) {
- dump(this.formatLog(str, level) + "\n");
- }
- break;
- default:
- // errorconsole
- if (this.loglevel <= level) {
- this._console.logStringMessage(this.formatLog(str, level));
- }
- break;
- }
- },
-
- safe_log(level, str, scrub) {
- if (this.loglevel < 4) {
- this.eclog(level, str + scrub);
- } else {
- this.eclog(level, str + " [scrubbed]");
- }
- },
-
- log(level, str) {
- switch (this.logmethod) {
- case 2: // debuglogger
- if (this._debuglog) {
- this._debuglog.log(6 - level, this.formatLog(str, level));
- break;
- }
- // fallthrough
- case 0: // stderr
- if (this.loglevel <= level) {
- dump(this.formatLog(str, level) + "\n");
- }
- break;
- case 1: // errorconsole
- if (this.loglevel <= level) {
- this._console.logStringMessage(this.formatLog(str, level));
- }
- break;
- default:
- dump("Bad log method: " + this.logmethod);
- }
- },
-
- // Pref observer interface implementation
-
- // topic: what event occurred
- // subject: what nsIPrefBranch we're observing
- // data: which pref has been changed (relative to subject)
- observe(subject, topic, data) {
- if (topic != "nsPref:changed") {
- return;
- }
- switch (data) {
- case "extensions.torbutton.logmethod":
- this.logmethod = Services.prefs.getIntPref(
- "extensions.torbutton.logmethod"
- );
- if (this.logmethod === 0) {
- Services.prefs.setBoolPref("browser.dom.window.dump.enabled", true);
- } else if (
- Services.prefs.getIntPref("extensions.torlauncher.logmethod", 3) !== 0
- ) {
- // If Tor Launcher is not available or its log method is not 0
- // then let's reset the dump pref.
- Services.prefs.setBoolPref("browser.dom.window.dump.enabled", false);
- }
- break;
- case "extensions.torbutton.loglevel":
- this.loglevel = Services.prefs.getIntPref(
- "extensions.torbutton.loglevel"
- );
- break;
- }
- },
-};
=====================================
toolkit/torbutton/modules/tor-control-port.js deleted
=====================================
@@ -1,1002 +0,0 @@
-// A module for TorBrowser that provides an asynchronous controller for
-// Tor, through its ControlPort.
-//
-// This file is written in call stack order (later functions
-// call earlier functions). The file can be processed
-// with docco.js to produce pretty documentation.
-//
-// To import the module, use
-//
-// let { configureControlPortModule, controller, wait_for_controller } =
-// Components.utils.import("path/to/tor-control-port.js", {});
-//
-// See the third-to-last function defined in this file:
-// configureControlPortModule(ipcFile, host, port, password)
-// for usage of the configureControlPortModule function.
-//
-// See the last functions defined in this file:
-// controller(avoidCache), wait_for_controller(avoidCache)
-// for usage of the controller functions.
-
-/* jshint esnext: true */
-/* jshint -W097 */
-/* global console */
-"use strict";
-
-const { XPCOMUtils } = ChromeUtils.importESModule(
- "resource://gre/modules/XPCOMUtils.sys.mjs"
-);
-
-ChromeUtils.defineModuleGetter(
- this,
- "TorMonitorService",
- "resource://gre/modules/TorMonitorService.jsm"
-);
-
-XPCOMUtils.defineLazyServiceGetter(
- this,
- "logger",
- "@torproject.org/torbutton-logger;1",
- "nsISupports"
-);
-
-// tor-launcher observer topics
-const TorTopics = Object.freeze({
- ProcessIsReady: "TorProcessIsReady",
-});
-
-// __log__.
-// Logging function
-let log = x =>
- logger.wrappedJSObject.eclog(3, x.trimRight().replace(/\r\n/g, "\n"));
-
-// ### announce this file
-log("Loading tor-control-port.js\n");
-
-class AsyncSocket {
- constructor(ipcFile, host, port) {
- let sts = Cc["@mozilla.org/network/socket-transport-service;1"].getService(
- Ci.nsISocketTransportService
- );
- const OPEN_UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
-
- let socketTransport = ipcFile
- ? sts.createUnixDomainTransport(ipcFile)
- : sts.createTransport([], host, port, null, null);
-
- this.outputStream = socketTransport
- .openOutputStream(OPEN_UNBUFFERED, 1, 1)
- .QueryInterface(Ci.nsIAsyncOutputStream);
- this.outputQueue = [];
-
- 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);
- this.inputQueue = [];
- }
-
- // asynchronously write string to underlying socket and return number of bytes written
- async write(str) {
- return new Promise((resolve, reject) => {
- // asyncWait next write request
- const tryAsyncWait = () => {
- if (this.outputQueue.length) {
- this.outputStream.asyncWait(
- this.outputQueue.at(0), // next request
- 0,
- 0,
- Services.tm.currentThread
- );
- }
- };
-
- // output stream can only have 1 registered callback at a time, so multiple writes
- // need to be queued up (see nsIAsyncOutputStream.idl)
- this.outputQueue.push({
- // Implement an nsIOutputStreamCallback:
- onOutputStreamReady: () => {
- try {
- let bytesWritten = this.outputStream.write(str, str.length);
-
- // remove this callback object from queue as it is now completed
- this.outputQueue.shift();
-
- // request next wait if there is one
- tryAsyncWait();
-
- // finally resolve promise
- resolve(bytesWritten);
- } catch (err) {
- // reject 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
- 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: stream => {
- 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
- let str = this.scriptableInputStream.read(
- this.scriptableInputStream.available()
- );
-
- // remove this callback object from queue now that we have read
- this.inputQueue.shift();
-
- // request next wait if there is one
- tryAsyncWait();
-
- // finally resolve promise
- resolve(str);
- } catch (err) {
- 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() {
- this.outputStream.close();
- this.inputStream.close();
- }
-}
-
-class ControlSocket {
- constructor(asyncSocket) {
- this.socket = asyncSocket;
- this._isOpen = true;
- this.pendingData = "";
- this.pendingLines = [];
-
- this.mainDispatcher = io.callbackDispatcher();
- this.notificationDispatcher = io.callbackDispatcher();
- // mainDispatcher pushes only async notifications (650) to notificationDispatcher
- this.mainDispatcher.addCallback(
- /^650/,
- this._handleNotification.bind(this)
- );
- // callback for handling responses and errors
- this.mainDispatcher.addCallback(
- /^[245]\d\d/,
- this._handleCommandReply.bind(this)
- );
-
- this.commandQueue = [];
-
- this._startMessagePump();
- }
-
- // blocks until an entire line is read and returns it
- // immediately returns next line in queue (pendingLines) if present
- async _readLine() {
- // keep reading from socket until we have a full line to return
- while (!this.pendingLines.length) {
- // read data from our socket and spit on newline tokens
- this.pendingData += await this.socket.read();
- let 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
- 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.match(/^\.$/)) {
- 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://trac.torproject.org/16990#comment:28
- // 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 (note the space character at end of the regex)
- else if (line.match(/^\d\d\d /)) {
- if (message.length == 1) {
- endOfMessageFound = true;
- } else {
- let firstReplyCode = message[0].substring(0, 3);
- let lastReplyCode = line.substring(0, 3);
- if (firstReplyCode == lastReplyCode) {
- endOfMessageFound = true;
- }
- }
- }
- }
- } while (!endOfMessageFound);
-
- // join our lines back together to form one message
- return message.join("\r\n");
- }
-
- async _startMessagePump() {
- try {
- while (true) {
- let message = await this._readMessage();
- log("controlPort >> " + message);
- this.mainDispatcher.pushMessage(message);
- }
- } catch (err) {
- this._isOpen = false;
- for (const cmd of this.commandQueue) {
- cmd.reject(err);
- }
- this.commandQueue = [];
- }
- }
-
- _writeNextCommand() {
- let cmd = this.commandQueue[0];
- log("controlPort << " + cmd.commandString);
- this.socket.write(`${cmd.commandString}\r\n`).catch(cmd.reject);
- }
-
- async sendCommand(commandString) {
- if (!this.isOpen()) {
- throw new Error("ControlSocket not open");
- }
-
- // this promise is resolved either in _handleCommandReply, or
- // in _startMessagePump (on stream error)
- return new Promise((resolve, reject) => {
- let command = {
- commandString,
- resolve,
- reject,
- };
-
- this.commandQueue.push(command);
- if (this.commandQueue.length == 1) {
- this._writeNextCommand();
- }
- });
- }
-
- _handleCommandReply(message) {
- let cmd = this.commandQueue.shift();
- if (message.match(/^2/)) {
- cmd.resolve(message);
- } else if (message.match(/^[45]/)) {
- let myErr = new Error(cmd.commandString + " -> " + message);
- // Add Tor-specific information to the Error object.
- let idx = message.indexOf(" ");
- if (idx > 0) {
- myErr.torStatusCode = message.substring(0, idx);
- myErr.torMessage = message.substring(idx);
- } else {
- myErr.torStatusCode = message;
- }
- cmd.reject(myErr);
- } else {
- cmd.reject(
- new Error(
- `ControlSocket::_handleCommandReply received unexpected message:\n----\n${message}\n----`
- )
- );
- }
-
- // send next command if one is available
- if (this.commandQueue.length) {
- this._writeNextCommand();
- }
- }
-
- _handleNotification(message) {
- this.notificationDispatcher.pushMessage(message);
- }
-
- close() {
- this.socket.close();
- this._isOpen = false;
- }
-
- addNotificationCallback(regex, callback) {
- this.notificationDispatcher.addCallback(regex, callback);
- }
-
- isOpen() {
- return this._isOpen;
- }
-}
-
-// ## io
-// I/O utilities namespace
-
-let io = {};
-
-// __io.callbackDispatcher()__.
-// Returns dispatcher object with three member functions:
-// dispatcher.addCallback(regex, callback), dispatcher.removeCallback(callback),
-// and dispatcher.pushMessage(message).
-// Pass pushMessage to another function that needs a callback with a single string
-// argument. Whenever dispatcher.pushMessage receives a string, the dispatcher will
-// check for any regex matches and pass the string on to the corresponding callback(s).
-io.callbackDispatcher = function () {
- let callbackPairs = [],
- removeCallback = function (aCallback) {
- callbackPairs = callbackPairs.filter(function ([regex, callback]) {
- return callback !== aCallback;
- });
- },
- addCallback = function (regex, callback) {
- if (callback) {
- callbackPairs.push([regex, callback]);
- }
- return function () {
- removeCallback(callback);
- };
- },
- pushMessage = function (message) {
- for (let [regex, callback] of callbackPairs) {
- if (message.match(regex)) {
- callback(message);
- }
- }
- };
- return {
- pushMessage,
- removeCallback,
- addCallback,
- };
-};
-
-// __io.controlSocket(ipcFile, host, port, password)__.
-// Instantiates and returns a socket to a tor ControlPort at ipcFile or
-// host:port, authenticating with the given password. Example:
-//
-// // Open the socket
-// let socket = await io.controlSocket(undefined, "127.0.0.1", 9151, "MyPassw0rd");
-// // Send command and receive "250" response reply or error is thrown
-// await socket.sendCommand(commandText);
-// // Register or deregister for "650" notifications
-// // that match regex
-// socket.addNotificationCallback(regex, callback);
-// socket.removeNotificationCallback(callback);
-// // Close the socket permanently
-// socket.close();
-io.controlSocket = async function (ipcFile, host, port, password) {
- let socket = new AsyncSocket(ipcFile, host, port);
- let controlSocket = new ControlSocket(socket);
-
- // Log in to control port.
- await controlSocket.sendCommand("authenticate " + (password || ""));
- // Activate needed events.
- await controlSocket.sendCommand("setevents stream");
-
- return controlSocket;
-};
-
-// ## utils
-// A namespace for utility functions
-let utils = {};
-
-// __utils.identity(x)__.
-// Returns its argument unchanged.
-utils.identity = function (x) {
- return x;
-};
-
-// __utils.isString(x)__.
-// Returns true iff x is a string.
-utils.isString = function (x) {
- return typeof x === "string" || x instanceof String;
-};
-
-// __utils.capture(string, regex)__.
-// Takes a string and returns an array of capture items, where regex must have a single
-// capturing group and use the suffix /.../g to specify a global search.
-utils.capture = function (string, regex) {
- let matches = [];
- // Special trick to use string.replace for capturing multiple matches.
- string.replace(regex, function (a, captured) {
- matches.push(captured);
- });
- return matches;
-};
-
-// __utils.extractor(regex)__.
-// Returns a function that takes a string and returns an array of regex matches. The
-// regex must use the suffix /.../g to specify a global search.
-utils.extractor = function (regex) {
- return function (text) {
- return utils.capture(text, regex);
- };
-};
-
-// __utils.splitLines(string)__.
-// Splits a string into an array of strings, each corresponding to a line.
-utils.splitLines = function (string) {
- return string.split(/\r?\n/);
-};
-
-// __utils.splitAtSpaces(string)__.
-// Splits a string into chunks between spaces. Does not split at spaces
-// inside pairs of quotation marks.
-utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
-
-// __utils.splitAtFirst(string, regex)__.
-// Splits a string at the first instance of regex match. If no match is
-// found, returns the whole string.
-utils.splitAtFirst = function (string, regex) {
- let match = string.match(regex);
- return match
- ? [
- string.substring(0, match.index),
- string.substring(match.index + match[0].length),
- ]
- : string;
-};
-
-// __utils.splitAtEquals(string)__.
-// Splits a string into chunks between equals. Does not split at equals
-// inside pairs of quotation marks.
-utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g);
-
-// __utils.mergeObjects(arrayOfObjects)__.
-// Takes an array of objects like [{"a":"b"},{"c":"d"}] and merges to a single object.
-// Pure function.
-utils.mergeObjects = function (arrayOfObjects) {
- let result = {};
- for (let obj of arrayOfObjects) {
- for (let key in obj) {
- result[key] = obj[key];
- }
- }
- return result;
-};
-
-// __utils.listMapData(parameterString, listNames)__.
-// Takes a list of parameters separated by spaces, of which the first several are
-// unnamed, and the remainder are named, in the form `NAME=VALUE`. Apply listNames
-// to the unnamed parameters, and combine them in a map with the named parameters.
-// Example: `40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH`
-//
-// utils.listMapData("40 FAILED 0 95.78.59.36:80 REASON=CANT_ATTACH",
-// ["streamID", "event", "circuitID", "IP"])
-// // --> {"streamID" : "40", "event" : "FAILED", "circuitID" : "0",
-// // "address" : "95.78.59.36:80", "REASON" : "CANT_ATTACH"}"
-utils.listMapData = function (parameterString, listNames) {
- // Split out the space-delimited parameters.
- let parameters = utils.splitAtSpaces(parameterString),
- dataMap = {};
- // Assign listNames to the first n = listNames.length parameters.
- for (let i = 0; i < listNames.length; ++i) {
- dataMap[listNames[i]] = parameters[i];
- }
- // Read key-value pairs and copy these to the dataMap.
- for (let i = listNames.length; i < parameters.length; ++i) {
- let [key, value] = utils.splitAtEquals(parameters[i]);
- if (key && value) {
- dataMap[key] = value;
- }
- }
- return dataMap;
-};
-
-// __utils.rejectPromise(errorMessage)__.
-// Returns a rejected promise with the given error message.
-utils.rejectPromise = errorMessage => Promise.reject(new Error(errorMessage));
-
-// ## info
-// A namespace for functions related to tor's GETINFO and GETCONF command.
-let info = {};
-
-// __info.keyValueStringsFromMessage(messageText)__.
-// Takes a message (text) response to GETINFO or GETCONF and provides
-// a series of key-value strings, which are either multiline (with a `250+` prefix):
-//
-// 250+config/defaults=
-// AccountingMax "0 bytes"
-// AllowDotExit "0"
-// .
-//
-// or single-line (with a `250-` or `250 ` prefix):
-//
-// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
-info.keyValueStringsFromMessage = utils.extractor(
- /^(250\+[\s\S]+?^\.|250[- ].+?)$/gim
-);
-
-// __info.applyPerLine(transformFunction)__.
-// Returns a function that splits text into lines,
-// and applies transformFunction to each line.
-info.applyPerLine = function (transformFunction) {
- return function (text) {
- return utils.splitLines(text.trim()).map(transformFunction);
- };
-};
-
-// __info.routerStatusParser(valueString)__.
-// Parses a router status entry as, described in
-// https://gitweb.torproject.org/torspec.git/tree/dir-spec.txt
-// (search for "router status entry")
-info.routerStatusParser = function (valueString) {
- let lines = utils.splitLines(valueString),
- objects = [];
- for (let line of lines) {
- // Drop first character and grab data following it.
- let myData = line.substring(2),
- // Accumulate more maps with data, depending on the first character in the line.
- dataFun = {
- r: data =>
- utils.listMapData(data, [
- "nickname",
- "identity",
- "digest",
- "publicationDate",
- "publicationTime",
- "IP",
- "ORPort",
- "DirPort",
- ]),
- a: data => ({ IPv6: data }),
- s: data => ({ statusFlags: utils.splitAtSpaces(data) }),
- v: data => ({ version: data }),
- w: data => utils.listMapData(data, []),
- p: data => ({ portList: data.split(",") }),
- }[line.charAt(0)];
- if (dataFun !== undefined) {
- objects.push(dataFun(myData));
- }
- }
- return utils.mergeObjects(objects);
-};
-
-// __info.circuitStatusParser(line)__.
-// Parse the output of a circuit status line.
-info.circuitStatusParser = function (line) {
- let data = utils.listMapData(line, ["id", "status", "circuit"]),
- circuit = data.circuit;
- // Parse out the individual circuit IDs and names.
- if (circuit) {
- data.circuit = circuit.split(",").map(function (x) {
- return x.split(/~|=/);
- });
- }
- return data;
-};
-
-// __info.streamStatusParser(line)__.
-// Parse the output of a stream status line.
-info.streamStatusParser = function (text) {
- return utils.listMapData(text, [
- "StreamID",
- "StreamStatus",
- "CircuitID",
- "Target",
- ]);
-};
-
-// TODO: fix this parsing logic to handle bridgeLine correctly
-// fingerprint/id is an optional parameter
-// __info.bridgeParser(bridgeLine)__.
-// Takes a single line from a `getconf bridge` result and returns
-// a map containing the bridge's type, address, and ID.
-info.bridgeParser = function (bridgeLine) {
- let result = {},
- tokens = bridgeLine.split(/\s+/);
- // First check if we have a "vanilla" bridge:
- if (tokens[0].match(/^\d+\.\d+\.\d+\.\d+/)) {
- result.type = "vanilla";
- [result.address, result.ID] = tokens;
- // Several bridge types have a similar format:
- } else {
- result.type = tokens[0];
- if (
- [
- "flashproxy",
- "fte",
- "meek",
- "meek_lite",
- "obfs3",
- "obfs4",
- "scramblesuit",
- "snowflake",
- ].includes(result.type)
- ) {
- [result.address, result.ID] = tokens.slice(1);
- }
- }
- return result.type ? result : null;
-};
-
-// __info.parsers__.
-// A map of GETINFO and GETCONF keys to parsing function, which convert
-// result strings to JavaScript data.
-info.parsers = {
- "ns/id/": info.routerStatusParser,
- "ip-to-country/": utils.identity,
- "circuit-status": info.applyPerLine(info.circuitStatusParser),
- bridge: info.bridgeParser,
- // Currently unused parsers:
- // "ns/name/" : info.routerStatusParser,
- // "stream-status" : info.applyPerLine(info.streamStatusParser),
- // "version" : utils.identity,
- // "config-file" : utils.identity,
-};
-
-// __info.getParser(key)__.
-// Takes a key and determines the parser function that should be used to
-// convert its corresponding valueString to JavaScript data.
-info.getParser = function (key) {
- return (
- info.parsers[key] ||
- info.parsers[key.substring(0, key.lastIndexOf("/") + 1)]
- );
-};
-
-// __info.stringToValue(string)__.
-// Converts a key-value string as from GETINFO or GETCONF to a value.
-info.stringToValue = function (string) {
- // key should look something like `250+circuit-status=` or `250-circuit-status=...`
- // or `250 circuit-status=...`
- let matchForKey = string.match(/^250[ +-](.+?)=/),
- key = matchForKey ? matchForKey[1] : null;
- if (key === null) {
- return null;
- }
- // matchResult finds a single-line result for `250-` or `250 `,
- // or a multi-line one for `250+`.
- let matchResult =
- string.match(/^250[ -].+?=(.*)$/) ||
- string.match(/^250\+.+?=([\s\S]*?)^\.$/m),
- // Retrieve the captured group (the text of the value in the key-value pair)
- valueString = matchResult ? matchResult[1] : null,
- // Get the parser function for the key found.
- parse = info.getParser(key.toLowerCase());
- if (parse === undefined) {
- throw new Error("No parser found for '" + key + "'");
- }
- // Return value produced by the parser.
- return parse(valueString);
-};
-
-// __info.getMultipleResponseValues(message)__.
-// Process multiple responses to a GETINFO or GETCONF request.
-info.getMultipleResponseValues = function (message) {
- return info
- .keyValueStringsFromMessage(message)
- .map(info.stringToValue)
- .filter(utils.identity);
-};
-
-// __info.getInfo(controlSocket, key)__.
-// Sends GETINFO for a single key. Returns a promise with the result.
-info.getInfo = function (aControlSocket, key) {
- if (!utils.isString(key)) {
- return utils.rejectPromise("key argument should be a string");
- }
- return aControlSocket
- .sendCommand("getinfo " + key)
- .then(response => info.getMultipleResponseValues(response)[0]);
-};
-
-// __info.getConf(aControlSocket, key)__.
-// Sends GETCONF for a single key. Returns a promise with the result.
-info.getConf = function (aControlSocket, key) {
- // GETCONF with a single argument returns results with
- // one or more lines that look like `250[- ]key=value`.
- // Any GETCONF lines that contain a single keyword only are currently dropped.
- // So we can use similar parsing to that for getInfo.
- if (!utils.isString(key)) {
- return utils.rejectPromise("key argument should be a string");
- }
- return aControlSocket
- .sendCommand("getconf " + key)
- .then(info.getMultipleResponseValues);
-};
-
-// ## onionAuth
-// A namespace for functions related to tor's ONION_CLIENT_AUTH_* commands.
-let onionAuth = {};
-
-onionAuth.keyInfoStringsFromMessage = utils.extractor(/^250-CLIENT\s+(.+)$/gim);
-
-onionAuth.keyInfoObjectsFromMessage = function (message) {
- let keyInfoStrings = onionAuth.keyInfoStringsFromMessage(message);
- return keyInfoStrings.map(infoStr =>
- utils.listMapData(infoStr, ["hsAddress", "typeAndKey"])
- );
-};
-
-// __onionAuth.viewKeys()__.
-// Sends a ONION_CLIENT_AUTH_VIEW command to retrieve the list of private keys.
-// Returns a promise that is fulfilled with an array of key info objects which
-// contain the following properties:
-// hsAddress
-// typeAndKey
-// Flags (e.g., "Permanent")
-onionAuth.viewKeys = function (aControlSocket) {
- let cmd = "onion_client_auth_view";
- return aControlSocket
- .sendCommand(cmd)
- .then(onionAuth.keyInfoObjectsFromMessage);
-};
-
-// __onionAuth.add(controlSocket, hsAddress, b64PrivateKey, isPermanent)__.
-// Sends a ONION_CLIENT_AUTH_ADD command to add a private key to the
-// Tor configuration.
-onionAuth.add = function (
- aControlSocket,
- hsAddress,
- b64PrivateKey,
- isPermanent
-) {
- if (!utils.isString(hsAddress)) {
- return utils.rejectPromise("hsAddress argument should be a string");
- }
-
- if (!utils.isString(b64PrivateKey)) {
- return utils.rejectPromise("b64PrivateKey argument should be a string");
- }
-
- const keyType = "x25519";
- let cmd = `onion_client_auth_add ${hsAddress} ${keyType}:${b64PrivateKey}`;
- if (isPermanent) {
- cmd += " Flags=Permanent";
- }
- return aControlSocket.sendCommand(cmd);
-};
-
-// __onionAuth.remove(controlSocket, hsAddress)__.
-// Sends a ONION_CLIENT_AUTH_REMOVE command to remove a private key from the
-// Tor configuration.
-onionAuth.remove = function (aControlSocket, hsAddress) {
- if (!utils.isString(hsAddress)) {
- return utils.rejectPromise("hsAddress argument should be a string");
- }
-
- let cmd = `onion_client_auth_remove ${hsAddress}`;
- return aControlSocket.sendCommand(cmd);
-};
-
-// ## event
-// Handlers for events
-
-let event = {};
-
-// __event.parsers__.
-// A map of EVENT keys to parsing functions, which convert result strings to JavaScript
-// data.
-event.parsers = {
- stream: info.streamStatusParser,
- // Currently unused:
- // "circ" : info.circuitStatusParser,
-};
-
-// __event.messageToData(type, message)__.
-// Extract the data from an event. Note, at present
-// we only extract streams that look like `"650" SP...`
-event.messageToData = function (type, message) {
- let dataText = message.match(/^650 \S+?\s(.*)/m)[1];
- return dataText && type.toLowerCase() in event.parsers
- ? event.parsers[type.toLowerCase()](dataText)
- : null;
-};
-
-// __event.watchEvent(controlSocket, type, filter, onData)__.
-// Watches for a particular type of event. If filter(data) returns true, the event's
-// data is passed to the onData callback. Returns a zero arg function that
-// stops watching the event. Note: we only observe `"650" SP...` events
-// currently (no `650+...` or `650-...` events).
-event.watchEvent = function (controlSocket, type, filter, onData, raw = false) {
- controlSocket.addNotificationCallback(
- new RegExp("^650 " + type),
- function (message) {
- let data = event.messageToData(type, message);
- if (filter === null || filter(data)) {
- if (raw || !data) {
- onData(message);
- return;
- }
- onData(data);
- }
- }
- );
-};
-
-// ## tor
-// Things related to the main controller.
-let tor = {};
-
-// __tor.controllerCache__.
-// A map from "unix:socketpath" or "host:port" to controller objects. Prevents
-// redundant instantiation of control sockets.
-tor.controllerCache = new Map();
-
-// __tor.controller(ipcFile, host, port, password)__.
-// Creates a tor controller at the given ipcFile or host and port, with the
-// given password.
-tor.controller = async function (ipcFile, host, port, password) {
- let socket = await io.controlSocket(ipcFile, host, port, password);
- return {
- getInfo: key => info.getInfo(socket, key),
- getConf: key => info.getConf(socket, key),
- onionAuthViewKeys: () => onionAuth.viewKeys(socket),
- onionAuthAdd: (hsAddress, b64PrivateKey, isPermanent) =>
- onionAuth.add(socket, hsAddress, b64PrivateKey, isPermanent),
- onionAuthRemove: hsAddress => onionAuth.remove(socket, hsAddress),
- watchEvent: (type, filter, onData, raw = false) => {
- event.watchEvent(socket, type, filter, onData, raw);
- },
- isOpen: () => socket.isOpen(),
- close: () => {
- socket.close();
- },
- sendCommand: cmd => socket.sendCommand(cmd),
- };
-};
-
-// ## Export
-
-let controlPortInfo = {};
-
-// __configureControlPortModule(ipcFile, host, port, password)__.
-// Sets Tor control port connection parameters to be used in future calls to
-// the controller() function. Example:
-// configureControlPortModule(undefined, "127.0.0.1", 9151, "MyPassw0rd");
-var configureControlPortModule = function (ipcFile, host, port, password) {
- controlPortInfo.ipcFile = ipcFile;
- controlPortInfo.host = host;
- controlPortInfo.port = port || 9151;
- controlPortInfo.password = password;
-};
-
-// __controller(avoidCache)__.
-// Instantiates and returns a controller object that is connected and
-// authenticated to a Tor ControlPort using the connection parameters
-// provided in the most recent call to configureControlPortModule(), if
-// the controller doesn't yet exist. Otherwise returns the existing
-// controller to the given ipcFile or host:port. Throws on error.
-//
-// Example:
-//
-// // Get a new controller
-// const avoidCache = true;
-// let c = controller(avoidCache);
-// // Send command and receive `250` reply or error message in a promise:
-// let replyPromise = c.getInfo("ip-to-country/16.16.16.16");
-// // Close the controller permanently
-// c.close();
-var controller = async function (avoidCache) {
- if (!controlPortInfo.ipcFile && !controlPortInfo.host) {
- throw new Error("Please call configureControlPortModule first");
- }
-
- const dest = controlPortInfo.ipcFile
- ? `unix:${controlPortInfo.ipcFile.path}`
- : `${controlPortInfo.host}:${controlPortInfo.port}`;
-
- // constructor shorthand
- const newTorController = async () => {
- return tor.controller(
- controlPortInfo.ipcFile,
- controlPortInfo.host,
- controlPortInfo.port,
- controlPortInfo.password
- );
- };
-
- // avoid cache so always return a new controller
- if (avoidCache) {
- return newTorController();
- }
-
- // first check our cache and see if we already have one
- let cachedController = tor.controllerCache.get(dest);
- if (cachedController && cachedController.isOpen()) {
- return cachedController;
- }
-
- // create a new one and store in the map
- cachedController = await newTorController();
- // overwrite the close() function to prevent consumers from closing a shared/cached controller
- cachedController.close = () => {
- throw new Error("May not close cached Tor Controller as it may be in use");
- };
-
- tor.controllerCache.set(dest, cachedController);
- return cachedController;
-};
-
-// __wait_for_controller(avoidCache)
-// Same as controller() function, but explicitly waits until there is a tor daemon
-// to connect to (either launched by tor-launcher, or if we have an existing system
-// tor daemon)
-var wait_for_controller = function (avoidCache) {
- // if tor process is running (either ours or system) immediately return controller
- if (!TorMonitorService.ownsTorDaemon || TorMonitorService.isRunning) {
- return controller(avoidCache);
- }
-
- // otherwise we must wait for tor to finish launching before resolving
- return new Promise((resolve, reject) => {
- let observer = {
- observe: async (subject, topic, data) => {
- if (topic === TorTopics.ProcessIsReady) {
- try {
- resolve(await controller(avoidCache));
- } catch (err) {
- reject(err);
- }
- Services.obs.removeObserver(observer, TorTopics.ProcessIsReady);
- }
- },
- };
- Services.obs.addObserver(observer, TorTopics.ProcessIsReady);
- });
-};
-
-// Export functions for external use.
-var EXPORTED_SYMBOLS = [
- "configureControlPortModule",
- "controller",
- "wait_for_controller",
-];
=====================================
toolkit/torbutton/modules/utils.js deleted
=====================================
@@ -1,276 +0,0 @@
-// # Utils.js
-// Various helpful utility functions.
-
-// ### Import Mozilla Services
-const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
-
-// ## Pref utils
-
-// __prefs__. A shortcut to Mozilla Services.prefs.
-let prefs = Services.prefs;
-
-// __getPrefValue(prefName)__
-// Returns the current value of a preference, regardless of its type.
-var getPrefValue = function (prefName) {
- switch (prefs.getPrefType(prefName)) {
- case prefs.PREF_BOOL:
- return prefs.getBoolPref(prefName);
- case prefs.PREF_INT:
- return prefs.getIntPref(prefName);
- case prefs.PREF_STRING:
- return prefs.getCharPref(prefName);
- default:
- return null;
- }
-};
-
-// __bindPref(prefName, prefHandler, init)__
-// Applies prefHandler whenever the value of the pref changes.
-// If init is true, applies prefHandler to the current value.
-// Returns a zero-arg function that unbinds the pref.
-var bindPref = function (prefName, prefHandler, init = false) {
- let update = () => {
- prefHandler(getPrefValue(prefName));
- },
- observer = {
- observe(subject, topic, data) {
- if (data === prefName) {
- update();
- }
- },
- };
- prefs.addObserver(prefName, observer);
- if (init) {
- update();
- }
- return () => {
- prefs.removeObserver(prefName, observer);
- };
-};
-
-// __bindPrefAndInit(prefName, prefHandler)__
-// Applies prefHandler to the current value of pref specified by prefName.
-// Re-applies prefHandler whenever the value of the pref changes.
-// Returns a zero-arg function that unbinds the pref.
-var bindPrefAndInit = (prefName, prefHandler) =>
- bindPref(prefName, prefHandler, true);
-
-// ## Observers
-
-// __observe(topic, callback)__.
-// Observe the given topic. When notification of that topic
-// occurs, calls callback(subject, data). Returns a zero-arg
-// function that stops observing.
-var observe = function (topic, callback) {
- let observer = {
- observe(aSubject, aTopic, aData) {
- if (topic === aTopic) {
- callback(aSubject, aData);
- }
- },
- };
- Services.obs.addObserver(observer, topic);
- return () => Services.obs.removeObserver(observer, topic);
-};
-
-// ## Environment variables
-
-// __getEnv(name)__.
-// Reads the environment variable of the given name.
-var getEnv = function (name) {
- return Services.env.exists(name) ? Services.env.get(name) : undefined;
-};
-
-// __getLocale
-// Returns the app locale to be used in tor-related urls.
-var getLocale = function () {
- const locale = Services.locale.appLocaleAsBCP47;
- if (locale === "ja-JP-macos") {
- // We don't want to distinguish the mac locale.
- return "ja";
- }
- return locale;
-};
-
-// ## Windows
-
-// __dialogsByName__.
-// Map of window names to dialogs.
-let dialogsByName = {};
-
-// __showDialog(parent, url, name, features, arg1, arg2, ...)__.
-// Like window.openDialog, but if the window is already
-// open, just focuses it instead of opening a new one.
-var showDialog = function (parent, url, name, features) {
- let existingDialog = dialogsByName[name];
- if (existingDialog && !existingDialog.closed) {
- existingDialog.focus();
- return existingDialog;
- }
- let newDialog = parent.openDialog.apply(parent, Array.slice(arguments, 1));
- dialogsByName[name] = newDialog;
- return newDialog;
-};
-
-// ## Tor control protocol utility functions
-
-let _torControl = {
- // 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 Tor Launcher, the file components/tl-protocol.js also contains a
- // copy of _strUnescape().
- _strUnescape(aStr) {
- if (!aStr) {
- return aStr;
- }
-
- var len = aStr.length;
- if (len < 2 || '"' != aStr.charAt(0) || '"' != aStr.charAt(len - 1)) {
- return aStr;
- }
-
- const kHexRE = /[0-9A-Fa-f]{2}/;
- const kOctalRE = /[0-7]{3}/;
- var rv = "";
- var i = 1;
- var lastCharIndex = len - 2;
- while (i <= lastCharIndex) {
- var c = aStr.charAt(i);
- if ("\\" == c) {
- if (++i > lastCharIndex) {
- throw new Error("missing character after \\");
- }
-
- c = aStr.charAt(i);
- if ("n" == c) {
- rv += "\n";
- } else if ("r" == c) {
- rv += "\r";
- } else if ("t" == c) {
- rv += "\t";
- } else if ("x" == c) {
- if (i + 2 > lastCharIndex) {
- throw new Error("not enough hex characters");
- }
-
- let s = aStr.substr(i + 1, 2);
- if (!kHexRE.test(s)) {
- throw new Error("invalid hex characters");
- }
-
- let val = parseInt(s, 16);
- rv += String.fromCharCode(val);
- i += 3;
- } else if (this._isDigit(c)) {
- let s = aStr.substr(i, 3);
- if (i + 2 > lastCharIndex) {
- throw new Error("not enough octal characters");
- }
-
- if (!kOctalRE.test(s)) {
- throw new Error("invalid octal characters");
- }
-
- let val = parseInt(s, 8);
- rv += String.fromCharCode(val);
- i += 3;
- } // "\\" and others
- else {
- rv += c;
- ++i;
- }
- } else if ('"' == c) {
- throw new Error('unescaped " within string');
- } else {
- rv += c;
- ++i;
- }
- }
-
- // Convert from UTF-8 to Unicode. TODO: is UTF-8 always used in protocol?
- return decodeURIComponent(escape(rv));
- }, // _strUnescape()
-
- // Within Tor Launcher, the file components/tl-protocol.js also contains a
- // copy of _isDigit().
- _isDigit(aChar) {
- const kRE = /^\d$/;
- return aChar && kRE.test(aChar);
- },
-}; // _torControl
-
-// __unescapeTorString(str, resultObj)__.
-// Unescape Tor Control string str (removing surrounding "" and \ escapes).
-// Returns the unescaped string. Throws upon failure.
-var unescapeTorString = function (str) {
- return _torControl._strUnescape(str);
-};
-
-var m_tb_torlog = Cc["@torproject.org/torbutton-logger;1"].getService(
- Ci.nsISupports
-).wrappedJSObject;
-
-var m_tb_string_bundle = torbutton_get_stringbundle();
-
-function torbutton_safelog(nLevel, sMsg, scrub) {
- m_tb_torlog.safe_log(nLevel, sMsg, scrub);
- return true;
-}
-
-function torbutton_log(nLevel, sMsg) {
- m_tb_torlog.log(nLevel, sMsg);
-
- // So we can use it in boolean expressions to determine where the
- // short-circuit is..
- return true;
-}
-
-// load localization strings
-function torbutton_get_stringbundle() {
- var o_stringbundle = false;
-
- try {
- var oBundle = Services.strings;
- o_stringbundle = oBundle.createBundle(
- "chrome://torbutton/locale/torbutton.properties"
- );
- } catch (err) {
- o_stringbundle = false;
- }
- if (!o_stringbundle) {
- torbutton_log(5, "ERROR (init): failed to find torbutton-bundle");
- }
-
- return o_stringbundle;
-}
-
-function torbutton_get_property_string(propertyname) {
- try {
- if (!m_tb_string_bundle) {
- m_tb_string_bundle = torbutton_get_stringbundle();
- }
-
- return m_tb_string_bundle.GetStringFromName(propertyname);
- } catch (e) {
- torbutton_log(4, "Unlocalized string " + propertyname);
- }
-
- return propertyname;
-}
-
-// Export utility functions for external use.
-let EXPORTED_SYMBOLS = [
- "bindPref",
- "bindPrefAndInit",
- "getEnv",
- "getLocale",
- "getPrefValue",
- "observe",
- "showDialog",
- "show_torbrowser_manual",
- "unescapeTorString",
- "torbutton_safelog",
- "torbutton_log",
- "torbutton_get_property_string",
-];
=====================================
toolkit/torbutton/moz.build
=====================================
@@ -3,8 +3,4 @@
# 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/.
-JAR_MANIFESTS += ['jar.mn']
-
-XPCOM_MANIFESTS += [
- "components.conf",
-]
+JAR_MANIFESTS += ["jar.mn"]
=====================================
tools/lint/eslint/eslint-plugin-mozilla/lib/environments/browser-window.js
=====================================
@@ -90,11 +90,7 @@ function getGlobalScriptIncludes(scriptPath) {
"browser/components/screenshots/content/"
)
.replace("chrome://browser/content/", "browser/base/content/")
- .replace("chrome://global/content/", "toolkit/content/")
- .replace(
- "chrome://torbutton/content/",
- "toolkit/torbutton/chrome/content/"
- );
+ .replace("chrome://global/content/", "toolkit/content/");
for (let mapping of Object.getOwnPropertyNames(MAPPINGS)) {
if (sourceFile.includes(mapping)) {
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f67d72dd2bf84df5e55a06a723a6969b2dc5f1f9...9722ca2637a1a68efa7207600c897f786f3b7c19
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f67d72dd2bf84df5e55a06a723a6969b2dc5f1f9...9722ca2637a1a68efa7207600c897f786f3b7c19
You're receiving this email because of your account on gitlab.torproject.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.torproject.org/pipermail/tor-commits/attachments/20230804/1fbd82e6/attachment-0001.htm>
More information about the tor-commits
mailing list