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#no...). + // 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/f67d72d...
tor-commits@lists.torproject.org