[tbb-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:01 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/tbb-commits/attachments/20230804/994e8af4/attachment-0001.htm>


More information about the tbb-commits mailing list