[tor-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 tor-commits mailing list