commit 2c2c5a8aceae44c68915e8fe33bc3865f00b535c Author: Arthur Edelstein arthuredelstein@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"];