commit 35ea75545037574c568df386798efb6ea21790d5 Author: Arthur Edelstein arthuredelstein@gmail.com Date: Wed Nov 12 12:15:01 2014 -0800
#13671: fix circuit display when bridges are used --- src/chrome/content/tor-circuit-display.js | 142 ++++++++++++++++++---------- src/modules/tor-control-port.js | 146 +++++++++++++++++------------ 2 files changed, 178 insertions(+), 110 deletions(-)
diff --git a/src/chrome/content/tor-circuit-display.js b/src/chrome/content/tor-circuit-display.js index 0817aa6..caf6886 100644 --- a/src/chrome/content/tor-circuit-display.js +++ b/src/chrome/content/tor-circuit-display.js @@ -27,6 +27,7 @@ let createTorCircuitDisplay = (function () { // Mozilla utilities const Cu = Components.utils; Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/Task.jsm");
// Import the controller code. let { controller } = Cu.import("resource://torbutton/modules/tor-control-port.js"); @@ -39,51 +40,90 @@ let logger = Cc["@torproject.org/torbutton-logger;1"]
// A mutable map that stores the current nodes for each domain. let domainToNodeDataMap = {}, - // A mutable map that records what circuits are already known. - knownCircuitIDs = {}; + // A mutable map that reports `true` for IDs of "mature" circuits + // (those that have conveyed a stream).. + knownCircuitIDs = {}, + // A map from bridge fingerprint to its IP address. + bridgeIDtoIPmap = new Map();
// __trimQuotes(s)__. // Removes quotation marks around a quoted string. let trimQuotes = s => s ? s.match(/^"(.*)"$/)[1] : undefined;
-// 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); - }); - }); +// __readBridgeIPs(controller)__. +// Gets a map from bridge ID to bridge IP, and stores it +// in `bridgeIDtoIPmap`. +let readBridgeIPs = function (controller) { + Task.spawn(function* () { + let configText = yield controller.getInfo("config-text"), + bridgeEntries = configText.Bridge; + if (bridgeEntries) { + bridgeEntries.map(entry => { + let IPplusPort, ID, + tokens = entry.split(/\s+/); + // First check if we have a "vanilla" bridge: + if (tokens[0].match(/^\d+.\d+.\d+.\d+/)) { + [IPplusPort, ID] = tokens; + // Several bridge types have a similar format: + } else if (["fte", "obfs3", "obfs4", "scramblesuit"] + .indexOf(tokens[0]) >= 0) { + [IPplusPort, ID] = tokens.slice(1); + } + // (For now, we aren't dealing with meek bridges and flashproxy.) + if (IPplusPort && ID) { + let IP = IPplusPort.split(":")[0]; + bridgeIDtoIPmap.set(ID.toUpperCase(), IP); + } + }); + } + }).then(null, Cu.reportError); };
-// __nodeDataForCircuit(controller, circuitEvent, onResult)__. +// nodeDataForID(controller, id)__. +// Returns the type, IP and country code of a node with given ID. +// Example: `nodeData(controller, "20BC91DC525C3DC9974B29FBEAB51230DE024C44")` +// => `{ type : "default" , ip : "12.23.34.45" , countryCode : "fr" }` +let nodeDataForID = function* (controller, id) { + let result = {}; // ip, type, countryCode; + if (bridgeIDtoIPmap.has(id.toUpperCase())) { + result.ip = bridgeIDtoIPmap.get(id.toUpperCase()); + result.type = "bridge"; + } else { + // Get the IP address for the given node ID. + try { + let statusMap = yield controller.getInfo("ns/id/" + id); + result.ip = statusMap.IP; + } catch (e) { } + result.type = "default"; + } + if (result.ip) { + // Get the country code for the node's IP address. + try { + result.countryCode = yield controller.getInfo("ip-to-country/" + result.ip); + } catch (e) { } + } + return result; +}; + +// __nodeDataForCircuit(controller, circuitEvent)__. // Gets the information for a circuit. -let nodeDataForCircuit = function (controller, circuitEvent, onResult) { - let ids = circuitEvent.circuit.map(circ => circ[0]); - nodeDataForID(controller, ids, onResult); +let nodeDataForCircuit = function* (controller, circuitEvent) { + let rawIDs = circuitEvent.circuit.map(circ => circ[0]), + // Remove the leading '$' if present. + ids = rawIDs.map(id => id[0] === "$" ? id.substring(1) : id); + // Get the node data for all IDs in circuit. + return [for (id of ids) yield nodeDataForID(controller, id)]; };
-// __getCircuitStatusByID(aController, circuitID, onCircuitStatus)__ -// Returns the circuit status for the circuit with the given ID -// via onCircuitStatus(status). -let getCircuitStatusByID = function(aController, circuitID, onCircuitStatus) { - aController.getInfo("circuit-status", function (circuitStatuses) { - for (let circuitStatus of circuitStatuses) { - if (circuitStatus.id === circuitID) { - onCircuitStatus(circuitStatus); - } +// __getCircuitStatusByID(aController, circuitID)__ +// Returns the circuit status for the circuit with the given ID. +let getCircuitStatusByID = function* (aController, circuitID) { + let circuitStatuses = yield aController.getInfo("circuit-status"); + for (let circuitStatus of circuitStatuses) { + if (circuitStatus.id === circuitID) { + return circuitStatus; } - }); + } };
// __collectIsolationData(aController)__. @@ -95,20 +135,18 @@ let collectIsolationData = function (aController) { aController.watchEvent( "STREAM", streamEvent => streamEvent.StreamStatus === "SENTCONNECT", - function (streamEvent) { + streamEvent => Task.spawn(function* () { if (!knownCircuitIDs[streamEvent.CircuitID]) { logger.eclog(3, "streamEvent.CircuitID: " + streamEvent.CircuitID); knownCircuitIDs[streamEvent.CircuitID] = true; - getCircuitStatusByID(aController, streamEvent.CircuitID, function (circuitStatus) { - let domain = trimQuotes(circuitStatus.SOCKS_USERNAME); - if (domain) { - nodeDataForCircuit(aController, circuitStatus, function (nodeData) { - domainToNodeDataMap[domain] = nodeData; - }); - } - }); + let circuitStatus = yield getCircuitStatusByID(aController, streamEvent.CircuitID), + domain = trimQuotes(circuitStatus.SOCKS_USERNAME); + if (domain) { + let nodeData = yield nodeDataForCircuit(aController, circuitStatus); + domainToNodeDataMap[domain] = nodeData; + } } - }); + }).then(null, Cu.reportError)); };
// ## User interface @@ -122,7 +160,7 @@ let regionBundle = Services.strings.createBundle( // Convert a country code to a localized country name. // Example: `'de'` -> `'Deutschland'` in German locale. let localizedCountryNameFromCode = function (countryCode) { - if (typeof(countryCode) === "undefined") return ""; + if (typeof(countryCode) === "undefined") return undefined; try { return regionBundle.GetStringFromName(countryCode.toLowerCase()); } catch (e) { @@ -144,10 +182,13 @@ let showCircuitDisplay = function (show) { // `"France (12.34.56.78)"`. let nodeLines = function (nodeData) { let result = ["This browser"]; - for (let {ip, country} of nodeData) { - result.push(localizedCountryNameFromCode(country) + " (" + ip + ")"); + for (let {ip, countryCode, type} of nodeData) { + let bridge = type === "bridge"; + result.push((countryCode ? localizedCountryNameFromCode(countryCode) + : "Unknown country") + + " (" + (bridge ? "Bridge" : (ip || "IP unknown")) + ")"); } - result[4] = ("Internet"); + result[4] = "Internet"; return result; };
@@ -207,7 +248,9 @@ let syncDisplayWithSelectedTab = (function() { updateCircuitDisplay(); } else { // Stop syncing. - gBrowser.tabContainer.removeEventListener("TabSelect", listener1); + if (gBrowser.tabContainer) { + gBrowser.tabContainer.removeEventListener("TabSelect", listener1); + } gBrowser.removeTabsProgressListener(listener2); // Hide the display. showCircuitDisplay(false); @@ -269,6 +312,7 @@ let setupDisplay = function (host, port, password, enablePrefName) { logger.eclog(5, "Disabling tor display circuit because of an error."); stop(); }); + readBridgeIPs(myController); syncDisplayWithSelectedTab(true); collectIsolationData(myController); } diff --git a/src/modules/tor-control-port.js b/src/modules/tor-control-port.js index eae62f5..d356ebb 100644 --- a/src/modules/tor-control-port.js +++ b/src/modules/tor-control-port.js @@ -23,10 +23,16 @@ let {classes: Cc, interfaces: Ci, results: Cr, Constructor: CC, utils: Cu } = Co // ### Import Mozilla Services Cu.import("resource://gre/modules/Services.jsm");
-// ## torbutton logger -let logger = Cc["@torproject.org/torbutton-logger;1"] - .getService(Components.interfaces.nsISupports).wrappedJSObject, - log = x => logger.eclog(3, x); +// __log__. +// Logging function +let log; +if ((typeof console) !== "undefined") { + log = x => console.log(typeof(x) === "string" ? x : JSON.stringify(x)); +} else { + let logger = Cc["@torproject.org/torbutton-logger;1"] + .getService(Components.interfaces.nsISupports).wrappedJSObject; + log = x => logger.eclog(3, x); +}
// ### announce this file log("Loading tor-control-port.js\n"); @@ -58,8 +64,8 @@ io.asyncSocketStreams = function (host, port) { // 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"), + let ScriptableInputStream = Components.Constructor( + "@mozilla.org/scriptableinputstream;1", "nsIScriptableInputStream", "init"), scriptableInputStream = new ScriptableInputStream(inputStream), // A private method to read all data available on the input stream. readAll = function() { @@ -171,11 +177,12 @@ io.onLineFromOnMessage = function (onMessage) { };
// __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). +// Returns dispatcher object with three member functions: +// dispatcher.addCallback(regex, callback), and dispatcher.removeCallback(callback), +// and dispatcher.pushMessage(message). +// Pass pushMessage to another function that needs a callback with a single string +// argument. Whenever dispatcher.pushMessage receives a string, the dispatcher will +// check for any regex matches and pass the string on to the corresponding callback(s). io.callbackDispatcher = function () { let callbackPairs = [], removeCallback = function (aCallback) { @@ -189,34 +196,38 @@ io.callbackDispatcher = function () { } return function () { removeCallback(callback); }; }, - onString = function (message) { + pushMessage = function (message) { for (let [regex, callback] of callbackPairs) { if (message.match(regex)) { callback(message); } } }; - return [onString, {addCallback : addCallback, removeCallback : removeCallback}]; + return { pushMessage : pushMessage, removeCallback : removeCallback, + addCallback : addCallback }; };
-// __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) { +// __io.matchRepliesToCommands(asyncSend, dispatcher)__. +// Takes asyncSend(message), an asynchronous send function, and the callback +// displatcher, and returns a function Promise<response> sendCommand(command). +io.matchRepliesToCommands = function (asyncSend, dispatcher) { let commandQueue = [], - sendCommand = function (command, replyCallback) { - commandQueue.push([command, replyCallback]); + sendCommand = function (command, replyCallback, errorCallback) { + commandQueue.push([command, replyCallback, errorCallback]); asyncSend(command); - }, - onReply = function (reply) { - let [command, replyCallback] = commandQueue.shift(); - if (replyCallback) { replyCallback(reply); } - }, - onFailure = function () { - commandQueue.shift(); }; - return [sendCommand, onReply, onFailure]; + // Watch for responses (replies or error messages) + dispatcher.addCallback(/^[245]\d\d/, function (message) { + let [command, replyCallback, errorCallback] = commandQueue.shift(); + if (message.match(/^2/) && replyCallback) replyCallback(message); + if (message.match(/^[45]/) && errorCallback) { + errorCallback(new Error(command + " -> " + message)); + } + }); + // Create and return a version of sendCommand that returns a Promise. + return command => new Promise(function (replyCallback, errorCallback) { + sendCommand(command, replyCallback, errorCallback); + }); };
// __io.controlSocket(host, port, password, onError)__. @@ -228,7 +239,7 @@ io.matchRepliesToCommands = function (asyncSend) { // 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); +// socket.sendCommand(commandText, replyCallback, errorCallback); // // Register or deregister for "650" notifications // // that match regex // socket.addNotificationCallback(regex, callback); @@ -237,26 +248,20 @@ io.matchRepliesToCommands = function (asyncSend) { // socket.close(); io.controlSocket = function (host, port, password, onError) { // Produce a callback dispatcher for Tor messages. - let [onMessage, mainDispatcher] = io.callbackDispatcher(), + let mainDispatcher = io.callbackDispatcher(), // Open the socket and convert format to Tor messages. socket = io.asyncSocket(host, port, - io.onDataFromOnLine(io.onLineFromOnMessage(onMessage)), + io.onDataFromOnLine( + io.onLineFromOnMessage(mainDispatcher.pushMessage)), 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 sendCommand method from writeLine. + sendCommand = io.matchRepliesToCommands(writeLine, mainDispatcher), // 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)); - }); + notificationDispatcher = io.callbackDispatcher(); // Pass asynchronous notifications to notification dispatcher. - mainDispatcher.addCallback(/^650/, onNotification); + mainDispatcher.addCallback(/^650/, notificationDispatcher.pushMessage); // Log in to control port. sendCommand("authenticate " + (password || "")); // Activate needed events. @@ -310,6 +315,16 @@ utils.splitLines = function (string) { return string.split(/\r?\n/); }; // inside pairs of quotation marks. utils.splitAtSpaces = utils.extractor(/((\S*?"(.*?)")+\S*|\S+)/g);
+// __utils.splitAtFirst(string, regex)__. +// Splits a string at the first instance of regex match. If no match is +// found, returns the whole string. +utils.splitAtFirst = function (string, regex) { + let match = string.match(regex); + return match ? [ string.substring(0, match.index), + string.substring(match.index + match[0].length) ] + : string; +}; + // __utils.splitAtEquals(string)__. // Splits a string into chunks between equals. Does not split at equals // inside pairs of quotation marks. @@ -321,7 +336,7 @@ utils.splitAtEquals = utils.extractor(/(([^=]*?"(.*?)")+[^=]*|[^=]+)/g); utils.mergeObjects = function (arrayOfObjects) { let result = {}; for (let obj of arrayOfObjects) { - for (var key in obj) { + for (let key in obj) { result[key] = obj[key]; } } @@ -433,6 +448,20 @@ info.streamStatusParser = function (text) { "CircuitID", "Target"]); };
+// __info.configTextParser(text)__. +// Parse the output of a `getinfo config-text`. +info.configTextParser = function(text) { + let result = {}; + utils.splitLines(text).map(function(line) { + let [name, value] = utils.splitAtFirst(line, /\s/); + if (name) { + if (!result.hasOwnProperty(name)) result[name] = []; + result[name].push(value); + } + }); + return result; +}; + // __info.parsers__. // A map of GETINFO keys to parsing function, which convert result strings to JavaScript // data. @@ -440,7 +469,7 @@ info.parsers = { "version" : utils.identity, "config-file" : utils.identity, "config-defaults-file" : utils.identity, - "config-text" : utils.identity, + "config-text" : info.configTextParser, "ns/id/" : info.routerStatusParser, "ns/name/" : info.routerStatusParser, "ip-to-country/" : utils.identity, @@ -471,10 +500,9 @@ info.stringToValue = function (string) { 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) { +// __info.getInfoMultiple(aControlSocket, keys)__. +// Sends GETINFO for an array of keys. Returns a promise with an array of results. +info.getInfoMultiple = function (aControlSocket, keys) { /* if (!(keys instanceof Array)) { throw new Error("keys argument should be an array"); @@ -490,14 +518,14 @@ info.getInfoMultiple = function (aControlSocket, keys, onData) { throw new Error("unsupported key"); } */ - aControlSocket.sendCommand("getinfo " + keys.join(" "), function (message) { - onData(info.keyValueStringsFromMessage(message).map(info.stringToValue)); - }); + return aControlSocket.sendCommand("getinfo " + keys.join(" ")) + .then(message => 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) { +// __info.getInfo(controlSocket, key)__. +// Sends GETINFO for a single key. Returns a promise with the result. +info.getInfo = function (aControlSocket, key) { /* if (!utils.isString(key)) { throw new Error("key argument should be a string"); @@ -506,9 +534,7 @@ info.getInfo = function (aControlSocket, key, onValue) { throw new Error("onValue argument should be a function"); } */ - info.getInfoMultiple(aControlSocket, [key], function (data) { - onValue(data[0]); - }); + return info.getInfoMultiple(aControlSocket, [key]).then(data => data[0]); };
// ## event @@ -559,10 +585,8 @@ tor.controllerCache = {}; tor.controller = function (host, port, password, onError) { let socket = io.controlSocket(host, port, password, onError), isOpen = true; - return { getInfo : function (key, log) { info.getInfo(socket, key, log); } , - getInfoMultiple : function (keys, log) { - info.getInfoMultiple(socket, keys, log); - }, + return { getInfo : key => info.getInfo(socket, key), + getInfoMultiple : keys => info.getInfoMultiple(socket, keys), watchEvent : function (type, filter, onData) { event.watchEvent(socket, type, filter, onData); },