ma1 pushed to branch base-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
dc2274d0 by hackademix at 2023-08-08T09:18:01+02:00
amend! Bug 32308: use direct browser sizing for letterboxing.
Bug 32308: use direct browser sizing for letterboxing.
Bug 30556: align letterboxing with 200x100 new win width stepping
- - - - -
9ac98dab by hackademix at 2023-08-08T09:18:02+02:00
fixup! Firefox preference overrides.
Bug 33282: Redefine the dimensions of new RFP windows
- - - - -
2 changed files:
- browser/app/profile/001-base-profile.js
- toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
Changes:
=====================================
browser/app/profile/001-base-profile.js
=====================================
@@ -291,6 +291,9 @@ pref("dom.use_components_shim", false);
pref("privacy.resistFingerprinting.letterboxing", true);
// tor-browser#41695: how many warnings we show if user closes them without restoring the window size
pref("privacy.resistFingerprinting.resizeWarnings", 3);
+// tor-browser#33282: new windows start at 1400x900 when there's enough screen space, otherwise down by 200x100 blocks
+pref("privacy.window.maxInnerWidth", 1400);
+pref("privacy.window.maxInnerHeight", 900);
// Enforce Network Information API as disabled
pref("dom.netinfo.enabled", false);
pref("network.http.referer.defaultPolicy", 2); // Bug 32948: Make referer behavior consistent regardless of private browing mode status
=====================================
toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
=====================================
@@ -475,14 +475,14 @@ class _RFPHelper {
/**
* Given a width or height, rounds it with the proper stepping.
*/
- steppedSize(aDimension) {
+ steppedSize(aDimension, isWidth = false) {
let stepping;
if (aDimension <= 50) {
return 0;
} else if (aDimension <= 500) {
stepping = 50;
} else if (aDimension <= 1600) {
- stepping = 100;
+ stepping = isWidth ? 200 : 100;
} else {
stepping = 200;
}
@@ -569,7 +569,7 @@ class _RFPHelper {
// If the set is empty, we will round the content with the default
// stepping size.
if (!this._letterboxingDimensions.length) {
- result = r(this.steppedSize(aWidth), this.steppedSize(aHeight));
+ result = r(this.steppedSize(aWidth, true), this.steppedSize(aHeight));
log(
`${logPrefix} roundDimensions(${aWidth}, ${aHeight}) = ${result.width} x ${result.height}`
);
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/65183e…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/65183e…
You're receiving this email because of your account on gitlab.torproject.org.
ma1 pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
e0e763c0 by hackademix at 2023-08-08T09:11:30+02:00
fixup! Firefox preference overrides.
Bug 33282: Redefine the dimensions of new RFP windows
- - - - -
1 changed file:
- browser/app/profile/001-base-profile.js
Changes:
=====================================
browser/app/profile/001-base-profile.js
=====================================
@@ -291,6 +291,9 @@ pref("dom.use_components_shim", false);
pref("privacy.resistFingerprinting.letterboxing", true);
// tor-browser#41695: how many warnings we show if user closes them without restoring the window size
pref("privacy.resistFingerprinting.resizeWarnings", 3);
+// tor-browser#33282: new windows start at 1400x900 when there's enough screen space, otherwise down by 200x100 blocks
+pref("privacy.window.maxInnerWidth", 1400);
+pref("privacy.window.maxInnerHeight", 900);
// Enforce Network Information API as disabled
pref("dom.netinfo.enabled", false);
pref("network.http.referer.defaultPolicy", 2); // Bug 32948: Make referer behavior consistent regardless of private browing mode status
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/e0e763c…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/e0e763c…
You're receiving this email because of your account on gitlab.torproject.org.
ma1 pushed to branch tor-browser-115.1.0esr-13.0-1 at The Tor Project / Applications / Tor Browser
Commits:
63eecb92 by hackademix at 2023-08-08T08:08:43+02:00
amend! Bug 32308: use direct browser sizing for letterboxing.
Bug 32308: use direct browser sizing for letterboxing.
Bug 30556: align letterboxing with 200x100 new win width stepping
- - - - -
1 changed file:
- toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
Changes:
=====================================
toolkit/components/resistfingerprinting/RFPHelper.sys.mjs
=====================================
@@ -475,14 +475,14 @@ class _RFPHelper {
/**
* Given a width or height, rounds it with the proper stepping.
*/
- steppedSize(aDimension) {
+ steppedSize(aDimension, isWidth = false) {
let stepping;
if (aDimension <= 50) {
return 0;
} else if (aDimension <= 500) {
stepping = 50;
} else if (aDimension <= 1600) {
- stepping = 100;
+ stepping = isWidth ? 200 : 100;
} else {
stepping = 200;
}
@@ -569,7 +569,7 @@ class _RFPHelper {
// If the set is empty, we will round the content with the default
// stepping size.
if (!this._letterboxingDimensions.length) {
- result = r(this.steppedSize(aWidth), this.steppedSize(aHeight));
+ result = r(this.steppedSize(aWidth, true), this.steppedSize(aHeight));
log(
`${logPrefix} roundDimensions(${aWidth}, ${aHeight}) = ${result.width} x ${result.height}`
);
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/63eecb9…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/commit/63eecb9…
You're receiving this email because of your account on gitlab.torproject.org.
Pier Angelo Vendrame pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
a65bfce8 by Pier Angelo Vendrame at 2023-08-07T17:44:54+02:00
Bug 40855: Update toolchains for Firefox 115 (Android part)
Android-only toolchain updates.
- - - - -
301a540c by Pier Angelo Vendrame at 2023-08-07T17:44:56+02:00
Bug 40855: Updates for Firefox 115 (Application Services)
Application services needs NSS and SQLCipher.
We had two projects for them, but they are used only by AS.
So, our build scripts were a copy of Mozilla's, and we applied the same
patches.
This meant we needed to keep the build scripts up to date, with all the
additional changes for RBM.
Since no other project depended on them, we can build these libraries
here with Mozilla's scripts, without the need to keep theirs and ours
in sync.
In addition to that, this commit updates the list of Java dependencies.
- - - - -
b7d80c1d by Pier Angelo Vendrame at 2023-08-07T17:44:56+02:00
Bug 40867: Add a project for firefox-android.
This project replaces Android Components and Fenix.
- - - - -
13 changed files:
- Makefile
- − projects/android-components/build
- − projects/android-components/config
- − projects/android-components/gradle-dependencies-list.txt
- − projects/android-components/list_toolchain_updates_checks
- − projects/android-components/mavenLocal.patch
- projects/android-toolchain/build
- projects/android-toolchain/config
- + projects/application-services/apply-bug-13028.diff
- projects/application-services/bug40485.patch → projects/application-services/bug40485.diff
- projects/nss/bug_13028.patch → projects/application-services/bug_13028.patch
- projects/application-services/build
- projects/application-services/config
The diff was not included because it is too large.
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/compare/…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/compare/…
You're receiving this email because of your account on gitlab.torproject.org.
Pier Angelo Vendrame pushed to branch main at The Tor Project / Applications / tor-browser-build
Commits:
d6d756ac by Pier Angelo Vendrame at 2023-08-04T08:19:45+02:00
Bug 40880 (fix): Add zstd back again
The previous commit removed zstd from the list of the dependencies in
the README.
This commit restores it and sorts the dependencies.
- - - - -
1 changed file:
- README
Changes:
=====================================
README
=====================================
@@ -39,14 +39,14 @@ You also need a few perl modules installed:
If you are running Debian or Ubuntu, you can install them with:
-# apt-get install libyaml-libyaml-perl libtemplate-perl libdatetime-perl \
- libio-handle-util-perl libio-all-perl \
- libio-captureoutput-perl libjson-perl libpath-tiny-perl \
- libstring-shellquote-perl libsort-versions-perl \
- libdigest-sha-perl libdata-uuid-perl libdata-dump-perl \
- libfile-copy-recursive-perl libfile-slurp-perl git \
- mercurial uidmap libxml-writer-perl \
- libparallel-forkmanager-perl libxml-libxml-perl
+# apt-get install libdata-dump-perl libdata-uuid-perl libdatetime-perl \
+ libdigest-sha-perl libfile-copy-recursive-perl \
+ libfile-slurp-perl libio-all-perl libio-captureoutput-perl \
+ libio-handle-util-perl libjson-perl \
+ libparallel-forkmanager-perl libpath-tiny-perl \
+ libsort-versions-perl libstring-shellquote-perl \
+ libtemplate-perl libxml-libxml-perl libxml-writer-perl \
+ libyaml-libyaml-perl git mercurial uidmap zstd
If you are running an Arch based system, you should be able to install them with:
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/d…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser-build/-/commit/d…
You're receiving this email because of your account on gitlab.torproject.org.
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#n…).
+ // 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/f67d72…
--
View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/f67d72…
You're receiving this email because of your account on gitlab.torproject.org.