[tbb-commits] [torbutton/master] Bug #8641: TorButton popup menu that displays current Tor circuit
mikeperry at torproject.org
mikeperry at torproject.org
Thu Oct 30 21:06:24 UTC 2014
commit 2c2c5a8aceae44c68915e8fe33bc3865f00b535c
Author: Arthur Edelstein <arthuredelstein at gmail.com>
Date: Fri Aug 1 23:28:06 2014 -0700
Bug #8641: TorButton popup menu that displays current Tor circuit
---
src/chrome.manifest | 3 +-
src/chrome/content/popup.xul | 33 +-
src/chrome/content/tor-circuit-display.js | 185 ++++++++++
src/chrome/content/torbutton.js | 2 +
src/chrome/content/torbutton.xul | 1 +
src/chrome/skin/torbutton.css | 15 +
src/modules/tor-control-port.js | 575 +++++++++++++++++++++++++++++
7 files changed, 809 insertions(+), 5 deletions(-)
diff --git a/src/chrome.manifest b/src/chrome.manifest
index d211984..2ab3c8f 100644
--- a/src/chrome.manifest
+++ b/src/chrome.manifest
@@ -3,6 +3,7 @@ overlay chrome://browser/content/browser.xul chrome://torbutton/content/torbutto
overlay chrome://browser/content/preferences/connection.xul chrome://torbutton/content/pref-connection.xul
overlay chrome://messenger/content/messenger.xul chrome://torbutton/content/torbutton_tb.xul
overlay chrome://messenger/content/messengercompose/messengercompose.xul chrome://torbutton/content/torbutton_tb.xul
+resource torbutton ./
# browser branding
override chrome://branding/locale/brand.dtd chrome://torbutton/locale/brand.dtd
@@ -161,4 +162,4 @@ contract @torproject.org/domain-isolator;1 {e33fd6d4-270f-475f-a96f-ff3140279f68
category profile-after-change CookieJarSelector @torproject.org/cookie-jar-selector;1
category profile-after-change TBSessionBlocker @torproject.org/torbutton-ss-blocker;1
category profile-after-change StartupObserver @torproject.org/startup-observer;1
-category profile-after-change DomainIsolator @torproject.org/domain-isolator;1
\ No newline at end of file
+category profile-after-change DomainIsolator @torproject.org/domain-isolator;1
diff --git a/src/chrome/content/popup.xul b/src/chrome/content/popup.xul
index 3ee953b..2965ec5 100644
--- a/src/chrome/content/popup.xul
+++ b/src/chrome/content/popup.xul
@@ -9,14 +9,16 @@
<stringbundleset id="torbutton-stringbundleset">
<stringbundle id="torbutton-bundle" src="chrome://torbutton/locale/torbutton.properties"/>
</stringbundleset>
- <menupopup id="torbutton-context-menu" onpopupshowing="torbutton_check_protections();"
- anchor="torbutton-button" position="after_start">
+ <panel id="torbutton-context-menu" onpopupshowing="torbutton_check_protections();" titlebar="normal" noautohide="true"
+ anchor="torbutton-button" position="after_start" >
+ <hbox align="start">
+ <vbox>
<menuitem id="torbutton-new-identity"
label="&torbutton.context_menu.new_identity;"
accesskey="&torbutton.context_menu.new_identity_key;"
insertafter="context-stop"
oncommand="torbutton_new_identity()"/>
- <menuitem id="torbutton-cookie-protector"
+ <menuitem id="torbutton-cookie-protector"
label="&torbutton.context_menu.cookieProtections;"
accesskey="&torbutton.context_menu.cookieProtections.key;"
insertafter="context-stop"
@@ -42,6 +44,29 @@
insertafter="context-stop"
oncommand="torbutton_download_update()"
hidden="true"/>
- </menupopup>
+ </vbox>
+ <vbox>
+ <!-- The following SVG is used to display a Tor circuit diagram for the current tab.
+ It is not displayed unless activated by tor-circuit-display.js. -->
+ <svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full"
+ width="290" height="140" id="tor-circuit" style="display:none;">
+ <rect x="0" y="0" width="100%" height="100%" fill="#e8f4f4" />
+ <text id="title" style="font-size:14px;font-weight:bold;" x="10" y="20" fill="#2c26a7">Tor circuit for this site</text>
+ <text id="domain" style="font-size:13px;" x="10" y="38" fill="black">(trac.torproject.org):</text>
+ <rect x="18.5" width="3" y="56" height="64" fill="#4d363a" stroke-width="0"/>
+ <circle class="node-circle" cx="20" cy="56" r="4" />
+ <text class="node-text" x="32" y="56">This Browser</text>
+ <circle class="node-circle" cx="20" cy="72" r="4" />
+ <text class="node-text" x="32" y="72">Test123 (54.67.87.34)</text>
+ <circle class="node-circle" cx="20" cy="88" r="4" />
+ <text class="node-text" x="32" y="88">TestABC (121.4.56.67)</text>
+ <circle class="node-circle" cx="20" cy="104" r="4" />
+ <text class="node-text" x="32" y="104">TestXYZ (74.3.30.9)</text>
+ <circle class="node-circle" cx="20" cy="120" r="4" />
+ <text class="node-text" x="32" y="120">Internet</text>
+ </svg>
+ </vbox>
+ </hbox>
+ </panel>
</overlay>
diff --git a/src/chrome/content/tor-circuit-display.js b/src/chrome/content/tor-circuit-display.js
new file mode 100644
index 0000000..5f4d8bf
--- /dev/null
+++ b/src/chrome/content/tor-circuit-display.js
@@ -0,0 +1,185 @@
+// A script that automatically displays the Tor Circuit used for the
+// current domain for the currently selected tab.
+//
+// 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.
+//
+// This script is to be embedded in torbutton.xul. It defines a single global function,
+// runTorCircuitDisplay(host, port, password), which activates the automatic Tor
+// circuit display for the current tab and any future tabs.
+//
+// See https://trac.torproject.org/8641
+
+/* jshint esnext: true */
+/* global document, gBrowser, Components */
+
+// ### Main function
+// __runTorCircuitDisplay(host, port, password)__.
+// The single function we run to activate automatic display of the Tor circuit..
+let runTorCircuitDisplay = (function () {
+
+"use strict";
+
+// Mozilla utilities
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Import the controller code.
+let { controller } = Cu.import("resource://torbutton/modules/tor-control-port.js");
+
+// Make the TorButton logger available.
+let logger = Cc["@torproject.org/torbutton-logger;1"]
+ .getService(Components.interfaces.nsISupports).wrappedJSObject;
+
+// __regionBundle__.
+// A list of localized region (country) names.
+let regionBundle = Services.strings.createBundle(
+ "chrome://global/locale/regionNames.properties");
+
+// __localizedCountryNameFromCode(countryCode)__.
+// Convert a country code to a localized country name.
+// Example: `'de'` -> `'Deutschland'` in German locale.
+let localizedCountryNameFromCode = function (countryCode) {
+ try {
+ return regionBundle.GetStringFromName(countryCode.toLowerCase());
+ } catch (e) {
+ return countryCode.toUpperCase();
+ }
+};
+
+// __domainToNodeDataMap__.
+// A mutable map that stores the current nodes for each domain.
+let domainToNodeDataMap = {};
+
+// __trimQuotes(s)__.
+// Removes quotation marks around a quoted string.
+let trimQuotes = s => s.match(/^\"(.*)\"$/)[1];
+
+// nodeDataForID(controller, id, onResult)__.
+// Requests the IP, country code, and name of a node with given ID.
+// Returns result via onResult.
+// Example: nodeData(["20BC91DC525C3DC9974B29FBEAB51230DE024C44"], show);
+let nodeDataForID = function (controller, ids, onResult) {
+ let idRequests = ids.map(id => "ns/id/" + id);
+ controller.getInfoMultiple(idRequests, function (statusMaps) {
+ let IPs = statusMaps.map(statusMap => statusMap.IP),
+ countryRequests = IPs.map(ip => "ip-to-country/" + ip);
+ controller.getInfoMultiple(countryRequests, function (countries) {
+ let results = [];
+ for (let i = 0; i < ids.length; ++i) {
+ results.push({ name : statusMaps[i].nickname, id : ids[i] ,
+ ip : statusMaps[i].IP , country : countries[i] });
+ }
+ onResult(results);
+ });
+ });
+};
+
+// __nodeDataForCircuit(controller, circuitEvent, onResult)__.
+// Gets the information for a circuit.
+let nodeDataForCircuit = function (controller, circuitEvent, onResult) {
+ let ids = circuitEvent.circuit.map(circ => circ[0]);
+ nodeDataForID(controller, ids, onResult);
+};
+
+// __nodeLines(nodeData)__.
+// Takes a nodeData array of three items each like
+// `{ ip : "12.34.56.78", country : "fr" }`
+// and converts each node data to text, as
+// `"France (12.34.56.78)"`.
+let nodeLines = function (nodeData) {
+ let result = ["This browser"];
+ for (let {ip, country} of nodeData) {
+ result.push(localizedCountryNameFromCode(country) + " (" + ip + ")");
+ }
+ result.push("Internet");
+ return result;
+};
+
+// __updateCircuitDisplay()__.
+// Updates the Tor circuit display SVG, showing the current domain
+// and the relay nodes for that domain.
+let updateCircuitDisplay = function () {
+ let URI = gBrowser.selectedBrowser.currentURI,
+ domain = null,
+ nodeData = null;
+ // Try to get a domain for this URI. Otherwise it remains null.
+ try {
+ domain = URI.host;
+ } catch (e) { }
+ if (domain) {
+ // Check if we have anything to show for this domain.
+ nodeData = domainToNodeDataMap[domain];
+ if (nodeData) {
+ // Update the displayed domain.
+ document.querySelector("svg#tor-circuit text#domain").innerHTML = "(" + domain + "):";
+ // Update the displayed information for the relay nodes.
+ let diagramNodes = document.querySelectorAll("svg#tor-circuit text.node-text"),
+ lines = nodeLines(nodeData);
+ for (let i = 0; i < diagramNodes.length; ++i) {
+ diagramNodes[i].innerHTML = lines[i];
+ }
+ }
+ }
+ // Only show the Tor circuit if we have a domain and node data.
+ document.querySelector("svg#tor-circuit").style.display = (domain && nodeData) ?
+ 'block' : 'none';
+};
+
+// __collectBuiltCircuitData(aController)__.
+// Watches for CIRC BUILT events and records their data in the domainToNodeDataMap.
+let collectBuiltCircuitData = function (aController) {
+ aController.watchEvent(
+ "CIRC",
+ circuitEvent => circuitEvent.status === "EXTENDED" ||
+ circuitEvent.status === "BUILT",
+ function (circuitEvent) {
+ let domain = trimQuotes(circuitEvent.SOCKS_USERNAME);
+ if (domain) {
+ nodeDataForCircuit(aController, circuitEvent, function (nodeData) {
+ domainToNodeDataMap[domain] = nodeData;
+ updateCircuitDisplay();
+ });
+ } else {
+ updateCircuitDisplay();
+ }
+ });
+};
+
+// __syncDisplayWithSelectedTab()__.
+// We may have multiple tabs, but there is only one instance of TorButton's popup
+// panel for displaying the Tor circuit UI. Therefore we need to update the display
+// to show the currently selected tab at its current location.
+let syncDisplayWithSelectedTab = function () {
+ // Whenever a different tab is selected, change the circuit display
+ // to show the circuit for that tab's domain.
+ gBrowser.tabContainer.addEventListener("TabSelect", function (event) {
+ updateCircuitDisplay();
+ });
+ // If the currently selected tab has been sent to a new location,
+ // update the circuit to reflect that.
+ gBrowser.addTabsProgressListener({ onLocationChange : function (aBrowser) {
+ if (aBrowser == gBrowser.selectedBrowser) {
+ updateCircuitDisplay();
+ }
+ } });
+
+ // Get started with a correct display.
+ updateCircuitDisplay();
+};
+
+// __display(host, port, password)__.
+// The main function for activating automatic display of the Tor circuit.
+// A reference to this function (called runTorCircuitDisplay) is exported as a global.
+let display = function (host, port, password) {
+ let myController = controller(host, port || 9151, password, function (x) { logger.eclog(5, x); });
+ syncDisplayWithSelectedTab();
+ collectBuiltCircuitData(myController);
+};
+
+return display;
+
+// Finish runTorCircuitDisplay()
+})();
+
diff --git a/src/chrome/content/torbutton.js b/src/chrome/content/torbutton.js
index 7fddf07..5be2c6d 100644
--- a/src/chrome/content/torbutton.js
+++ b/src/chrome/content/torbutton.js
@@ -578,6 +578,8 @@ function torbutton_init() {
torbutton_update_statusbar(mode);
torbutton_notify_if_update_needed();
+ runTorCircuitDisplay(m_tb_control_host, m_tb_control_port, m_tb_control_pass);
+
torbutton_log(3, 'init completed');
}
diff --git a/src/chrome/content/torbutton.xul b/src/chrome/content/torbutton.xul
index 9e10b09..00dc6f0 100644
--- a/src/chrome/content/torbutton.xul
+++ b/src/chrome/content/torbutton.xul
@@ -11,6 +11,7 @@
<script src="chrome://torbutton/content/stanford-safecache.js" />
<script type="application/x-javascript" src="chrome://torbutton/content/torbutton_util.js" />
+ <script type="application/x-javascript" src="chrome://torbutton/content/tor-circuit-display.js" />
<script type="application/x-javascript" src="chrome://torbutton/content/torbutton.js" />
<script language="JavaScript">
//onLoad Hander
diff --git a/src/chrome/skin/torbutton.css b/src/chrome/skin/torbutton.css
index ef8abbc..f368c9c 100644
--- a/src/chrome/skin/torbutton.css
+++ b/src/chrome/skin/torbutton.css
@@ -104,3 +104,18 @@ statusbarpanel#plugins-status[status="0"] {
#torbutton-downloadUpdate {
font-weight: bold;
}
+
+svg.circuit text {
+ font-family: Arial;
+}
+
+svg#tor-circuit text.node-text {
+ dominant-baseline: central;
+ font-size: 14px;
+}
+
+svg#tor-circuit circle.node-circle {
+ stroke: #195021;
+ stroke-width: 2px;
+ fill: white;
+}
\ No newline at end of file
diff --git a/src/modules/tor-control-port.js b/src/modules/tor-control-port.js
new file mode 100644
index 0000000..2f993d7
--- /dev/null
+++ b/src/modules/tor-control-port.js
@@ -0,0 +1,575 @@
+// 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 { controller } = Components.utils.import("path/to/controlPort.jsm");
+//
+// See the last function defined in this file, controller(host, port, password, onError)
+// for usage of the controller function.
+
+/* jshint esnext: true */
+/* jshint -W097 */
+/* global Components, console */
+"use strict";
+
+// ### Mozilla Abbreviations
+let {classes: Cc, interfaces: Ci, results: Cr, Constructor: CC, utils: Cu } = Components;
+
+// ## io
+// I/O utilities namespace
+let io = io || {};
+
+// __io.asyncSocketStreams(host, port)__.
+// Creates a pair of asynchronous input and output streams for a socket at the
+// given host and port.
+io.asyncSocketStreams = function (host, port) {
+ let socketTransportService = Cc["@mozilla.org/network/socket-transport-service;1"]
+ .getService(Components.interfaces.nsISocketTransportService),
+ BLOCKING = Ci.nsITransport.OPEN_BLOCKING,
+ UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED,
+ // Create an instance of a socket transport.
+ socketTransport = socketTransportService.createTransport(null, 0, host, port, null),
+ // Open unbuffered synchronous outputStream.
+ outputStream = socketTransport.openOutputStream(BLOCKING | UNBUFFERED, 1, 1),
+ // Open unbuffered asynchronous inputStream.
+ inputStream = socketTransport.openInputStream(UNBUFFERED, 1, 1)
+ .QueryInterface(Ci.nsIAsyncInputStream);
+ return [inputStream, outputStream];
+};
+
+// __io.pumpInputStream(scriptableInputStream, onInputData, onError)__.
+// Run an "input stream pump" that takes an input stream and
+// asynchronously pumps incoming data to the onInputData callback.
+io.pumpInputStream = function (inputStream, onInputData, onError) {
+ // Wrap raw inputStream with a "ScriptableInputStream" so we can read incoming data.
+ let ScriptableInputStream = CC("@mozilla.org/scriptableinputstream;1",
+ "nsIScriptableInputStream", "init"),
+ scriptableInputStream = new ScriptableInputStream(inputStream),
+ // A private method to read all data available on the input stream.
+ readAll = function() {
+ return scriptableInputStream.read(scriptableInputStream.available());
+ },
+ pump = Cc["@mozilla.org/network/input-stream-pump;1"]
+ .createInstance(Components.interfaces.nsIInputStreamPump);
+ // Start the pump.
+ pump.init(inputStream, -1, -1, 0, 0, true);
+ // Tell the pump to read all data whenever it is available, and pass the data
+ // to the onInputData callback. The first argument to asyncRead implements
+ // nsIStreamListener.
+ pump.asyncRead({ onStartRequest: function (request, context) { },
+ onStopRequest: function (request, context, code) { },
+ onDataAvailable : function (request, context, stream, offset, count) {
+ try {
+ onInputData(readAll());
+ } catch (error) {
+ // readAll() or onInputData(...) has thrown an error.
+ // Notify calling code through onError.
+ onError(error);
+ }
+ } }, null);
+};
+
+// __io.asyncSocket(host, port, onInputData, onError)__.
+// Creates an asynchronous, text-oriented TCP socket at host:port.
+// The onInputData callback should accept a single argument, which will be called
+// repeatedly, whenever incoming text arrives. Returns a socket object with two methods:
+// socket.write(text) and socket.close(). onError will be passed the error object
+// whenever a write fails.
+io.asyncSocket = function (host, port, onInputData, onError) {
+ let [inputStream, outputStream] = io.asyncSocketStreams(host, port);
+ // Run an input stream pump to send incoming data to the onInputData callback.
+ io.pumpInputStream(inputStream, onInputData, onError);
+ return {
+ // Write a message to the socket.
+ write : function(aString) {
+ try {
+ outputStream.write(aString, aString.length);
+ // console.log(aString);
+ } catch (err) {
+ // This write() method is not necessarily called by a callback,
+ // but we pass any thrown errors to onError to ensure the socket
+ // error handling uses a consistent single path.
+ onError(err);
+ }
+ },
+ // Close the socket.
+ close : function () {
+ // Close stream objects.
+ inputStream.close();
+ outputStream.close();
+ }
+ };
+};
+
+// __io.onDataFromOnLine(onLine)__.
+// Converts a callback that expects incoming individual lines of text to a callback that
+// expects incoming raw socket string data.
+io.onDataFromOnLine = function (onLine) {
+ // A private variable that stores the last unfinished line.
+ let pendingData = "";
+ // Return a callback to be passed to io.asyncSocket. First, splits data into lines of
+ // text. If the incoming data is not terminated by CRLF, then the last
+ // unfinished line will be stored in pendingData, to be prepended to the data in the
+ // next call to onData. The already complete lines of text are then passed in sequence
+ // to onLine.
+ return function (data) {
+ let totalData = pendingData + data,
+ lines = totalData.split("\r\n"),
+ n = lines.length;
+ pendingData = lines[n - 1];
+ // Call onLine for all completed lines.
+ lines.slice(0,-1).map(onLine);
+ };
+};
+
+// __io.onLineFromOnMessage(onMessage)__.
+// Converts a callback that expects incoming control port multiline message strings to a
+// callback that expects individual lines.
+io.onLineFromOnMessage = function (onMessage) {
+ // A private variable that stores the last unfinished line.
+ let pendingLines = [];
+ // Return a callback that expects individual lines.
+ return function (line) {
+ // Add to the list of pending lines.
+ pendingLines.push(line);
+ // If line is the last in a message, then pass on the full multiline message.
+ if (line.match(/^\d\d\d /) && (pendingLines.length == 1 ||
+ pendingLines[0].startsWith(line.substring(0,3)))) {
+ // Combine pending lines to form message.
+ let message = pendingLines.join("\r\n");
+ // Wipe pendingLines before we call onMessage, in case onMessage throws an error.
+ pendingLines = [];
+ // Pass multiline message to onMessage.
+ onMessage(message);
+ // console.log(message);
+ }
+ };
+};
+
+// __io.callbackDispatcher()__.
+// Returns [onString, dispatcher] where the latter is an object with two member functions:
+// dispatcher.addCallback(regex, callback), and dispatcher.removeCallback(callback).
+// Pass onString to another function that needs a callback with a single string argument.
+// Whenever dispatcher.onString 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); };
+ },
+ onString = function (message) {
+ for (let [regex, callback] of callbackPairs) {
+ if (message.match(regex)) {
+ callback(message);
+ }
+ }
+ };
+ return [onString, {addCallback : addCallback, removeCallback : removeCallback}];
+};
+
+// __io.matchRepliesToCommands(asyncSend)__.
+// Takes asyncSend(message), an asynchronous send function, and returns two functions
+// sendCommand(command, replyCallback) and onReply(response). If we call sendCommand,
+// then when onReply is called, the corresponding replyCallback will be called.
+io.matchRepliesToCommands = function (asyncSend) {
+ let commandQueue = [],
+ sendCommand = function (command, replyCallback) {
+ commandQueue.push([command, replyCallback]);
+ asyncSend(command);
+ },
+ onReply = function (reply) {
+ let [command, replyCallback] = commandQueue.shift();
+ if (replyCallback) { replyCallback(reply); }
+ },
+ onFailure = function () {
+ commandQueue.shift();
+ };
+ return [sendCommand, onReply, onFailure];
+};
+
+// __io.controlSocket(host, port, password, onError)__.
+// Instantiates and returns a socket to a tor ControlPort at host:port,
+// authenticating with the given password. onError is called with an
+// error object as its single argument whenever an error occurs. Example:
+//
+// // Open the socket
+// let socket = controlSocket("127.0.0.1", 9151, "MyPassw0rd",
+// function (error) { console.log(error.message || error); });
+// // Send command and receive "250" reply or error message
+// socket.sendCommand(commandText, replyCallback);
+// // Register or deregister for "650" notifications
+// // that match regex
+// socket.addNotificationCallback(regex, callback);
+// socket.removeNotificationCallback(callback);
+// // Close the socket permanently
+// socket.close();
+io.controlSocket = function (host, port, password, onError) {
+ // Produce a callback dispatcher for Tor messages.
+ let [onMessage, mainDispatcher] = io.callbackDispatcher(),
+ // Open the socket and convert format to Tor messages.
+ socket = io.asyncSocket(host, port,
+ io.onDataFromOnLine(io.onLineFromOnMessage(onMessage)),
+ onError),
+ // Tor expects any commands to be terminated by CRLF.
+ writeLine = function (text) { socket.write(text + "\r\n"); },
+ // Ensure we return the correct reply for each sendCommand.
+ [sendCommand, onReply, onFailure] = io.matchRepliesToCommands(writeLine),
+ // Create a secondary callback dispatcher for Tor notification messages.
+ [onNotification, notificationDispatcher] = io.callbackDispatcher();
+ // Pass successful reply back to sendCommand callback.
+ mainDispatcher.addCallback(/^2\d\d/, onReply);
+ // Pass error message to sendCommand callback.
+ mainDispatcher.addCallback(/^[45]\d\d/, function (message) {
+ onFailure();
+ onError(new Error(message));
+ });
+ // Pass asynchronous notifications to notification dispatcher.
+ mainDispatcher.addCallback(/^650/, onNotification);
+ // Log in to control port.
+ sendCommand("authenticate " + (password || ""));
+ // Activate needed events.
+ sendCommand("setevents stream circ");
+ return { close : socket.close, sendCommand : sendCommand,
+ addNotificationCallback : notificationDispatcher.addCallback,
+ removeNotificationCallback : notificationDispatcher.removeCallback };
+};
+
+// ## utils
+// A namespace for utility functions
+let utils = 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.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 (var 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 command.
+let info = info || {};
+
+// __info.keyValueStringsFromMessage(messageText)__.
+// Takes a message (text) response to GETINFO 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-` prefix):
+//
+// 250-version=0.2.6.0-alpha-dev (git-b408125288ad6943)
+info.keyValueStringsFromMessage = utils.extractor(/^(250\+[\s\S]+?^\.|250-.+?)$/gmi);
+
+// __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/blob/HEAD:/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(",") }) ,
+ "m" : data => utils.listMapData(data, [])
+ }[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"]);
+};
+
+// __info.parsers__.
+// A map of GETINFO keys to parsing function, which convert result strings to JavaScript
+// data.
+info.parsers = {
+ "version" : utils.identity,
+ "config-file" : utils.identity,
+ "config-defaults-file" : utils.identity,
+ "config-text" : utils.identity,
+ "ns/id/" : info.routerStatusParser,
+ "ns/name/" : info.routerStatusParser,
+ "ip-to-country/" : utils.identity,
+ "circuit-status" : info.applyPerLine(info.circuitStatusParser),
+ "stream-status" : info.applyPerLine(info.streamStatusParser)
+};
+
+// __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)] ||
+ "unknown";
+};
+
+// __info.stringToValue(string)__.
+// Converts a key-value string as from GETINFO to a value.
+info.stringToValue = function (string) {
+ // key should look something like `250+circuit-status=` or `250-circuit-status=...`
+ let key = string.match(/^250[\+-](.+?)=/mi)[1],
+ // matchResult finds a single-line result for `250-` or a multi-line one for `250+`.
+ matchResult = string.match(/250\-.+?=(.*?)$/mi) ||
+ string.match(/250\+.+?=([\s\S]*?)^\.$/mi),
+ // Retrieve the captured group (the text of the value in the key-value pair)
+ valueString = matchResult ? matchResult[1] : null;
+ // Return value where the latter has been parsed according to the key requested.
+ return info.getParser(key)(valueString);
+};
+
+// __info.getInfoMultiple(aControlSocket, keys, onData)__.
+// Sends GETINFO for an array of keys. Passes onData an array of their respective results,
+// in order.
+info.getInfoMultiple = function (aControlSocket, keys, onData) {
+ /*
+ if (!(keys instanceof Array)) {
+ throw new Error("keys argument should be an array");
+ }
+ if (!(onData instanceof Function)) {
+ throw new Error("onData argument should be a function");
+ }
+ let parsers = keys.map(info.getParser);
+ if (parsers.indexOf("unknown") !== -1) {
+ throw new Error("unknown key");
+ }
+ if (parsers.indexOf("not supported") !== -1) {
+ throw new Error("unsupported key");
+ }
+ */
+ aControlSocket.sendCommand("getinfo " + keys.join(" "), function (message) {
+ onData(info.keyValueStringsFromMessage(message).map(info.stringToValue));
+ });
+};
+
+// __info.getInfo(controlSocket, key, onValue)__.
+// Sends GETINFO for a single key. Passes onValue the value for that key.
+info.getInfo = function (aControlSocket, key, onValue) {
+ /*
+ if (!utils.isString(key)) {
+ throw new Error("key argument should be a string");
+ }
+ if (!(onValue instanceof Function)) {
+ throw new Error("onValue argument should be a function");
+ }
+ */
+ info.getInfoMultiple(aControlSocket, [key], function (data) {
+ onValue(data[0]);
+ });
+};
+
+// ## event
+// Handlers for events
+
+let event = event || {};
+
+// __event.parsers__.
+// A map of EVENT keys to parsing functions, which convert result strings to JavaScript
+// data.
+event.parsers = {
+ "stream" : info.streamStatusParser,
+ "circ" : info.circuitStatusParser
+};
+
+// __event.messageToData(type, message)__.
+// Extract the data from an event.
+event.messageToData = function (type, message) {
+ let dataText = message.match(/^650 \S+?\s(.*?)$/mi)[1];
+ return dataText ? 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 pass to the onData callback.
+event.watchEvent = function (controlSocket, type, filter, onData) {
+ controlSocket.addNotificationCallback(new RegExp("^650." + type, "i"),
+ function (message) {
+ let data = event.messageToData(type, message);
+ if (filter === null || filter(data)) {
+ onData(data);
+ }
+ });
+};
+
+// ## tor
+// Things related to the main controller.
+let tor = tor || {};
+
+// __tor.controller(host, port, password, onError)__.
+// Creates a tor controller at the given host and port, with the given password.
+// onError returns asynchronously whenever a connection error occurs.
+tor.controller = function (host, port, password, onError) {
+ let socket = io.controlSocket(host, port, password, onError);
+ return { getInfo : function (key, log) { info.getInfo(socket, key, log); } ,
+ getInfoMultiple : function (keys, log) {
+ info.getInfoMultiple(socket, keys, log);
+ },
+ watchEvent : function (type, filter, onData) {
+ event.watchEvent(socket, type, filter, onData);
+ },
+ close : socket.close };
+};
+
+// __tor.controllerCache__.
+// A map from "host:port" to controller objects. Prevents redundant instantiation
+// of control sockets.
+tor.controllerCache = {};
+
+// ## Export
+
+// __controller(host, port, password, onError)__.
+// Instantiates and returns a controller object connected to a tor ControlPort
+// at host:port, authenticating with the given password, if the controller doesn't yet
+// exist. Otherwise returns the existing controller to the given host:port.
+// onError is called with an error object as its single argument whenever
+// an error occurs. Example:
+//
+// // Get the controller
+// let c = controller("127.0.0.1", 9151, "MyPassw0rd",
+// function (error) { console.log(error.message || error); });
+// // Send command and receive `250` reply or error message
+// c.getInfo("ip-to-country/16.16.16.16", console.log);
+// // Close the controller permanently
+// c.close();
+let controller = function (host, port, password, onError) {
+ let dest = host + ":" + port;
+ return (tor.controllerCache[dest] = tor.controllerCache[dest] ||
+ tor.controller(host, port, password, onError));
+};
+
+// Export the controller function for external use.
+var EXPORTED_SYMBOLS = ["controller"];
More information about the tbb-commits
mailing list