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