[tbb-commits] [torbutton/master] #13671: fix circuit display when bridges are used

gk at torproject.org gk at torproject.org
Tue Dec 2 20:36:33 UTC 2014


commit 35ea75545037574c568df386798efb6ea21790d5
Author: Arthur Edelstein <arthuredelstein at 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);
            },





More information about the tbb-commits mailing list