commit bdc987b01c977c177d567eb850ce4c98d02e3538 Author: Arthur Edelstein arthuredelstein@gmail.com Date: Wed Nov 5 23:21:59 2014 -0800
Bug #13672. bind tor circuit display to a pref --- src/chrome/content/tor-circuit-display.js | 256 +++++++++++++++++++---------- src/chrome/content/torbutton.js | 3 +- src/defaults/preferences/preferences.js | 1 + src/modules/tor-control-port.js | 33 ++-- 4 files changed, 188 insertions(+), 105 deletions(-)
diff --git a/src/chrome/content/tor-circuit-display.js b/src/chrome/content/tor-circuit-display.js index 49cd832..0817aa6 100644 --- a/src/chrome/content/tor-circuit-display.js +++ b/src/chrome/content/tor-circuit-display.js @@ -15,9 +15,12 @@ /* 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 () { +// __createTorCircuitDisplay(host, port, password, enablePrefName)__. +// The single function that prepares tor circuit display. Connects to a tor +// control port with the given host, port, and password, and binds to +// a named bool pref whose value determines whether the circuit display +// is enabled or disabled. +let createTorCircuitDisplay = (function () {
"use strict";
@@ -32,28 +35,12 @@ let { controller } = Cu.import("resource://torbutton/modules/tor-control-port.js 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) { - if (typeof(countryCode) === "undefined") return ""; - try { - return regionBundle.GetStringFromName(countryCode.toLowerCase()); - } catch (e) { - return countryCode.toUpperCase(); - } -}; +// ## Circuit/stream domain and node monitoring
-// __domainToNodeDataMap__. // A mutable map that stores the current nodes for each domain. -let domainToNodeDataMap = {}; - -let knownCircuitIDs = {}; +let domainToNodeDataMap = {}, + // A mutable map that records what circuits are already known. + knownCircuitIDs = {};
// __trimQuotes(s)__. // Removes quotation marks around a quoted string. @@ -86,6 +73,70 @@ let nodeDataForCircuit = function (controller, circuitEvent, onResult) { nodeDataForID(controller, ids, onResult); };
+// __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); + } + } + }); +}; + +// __collectIsolationData(aController)__. +// Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then +// we assume isolation settings (SOCKS username+password) are now fixed for the +// corresponding circuit. Whenever the first stream on a new circuit is seen, +// looks up u+p and records the node data in the domainToNodeDataMap. +let collectIsolationData = function (aController) { + aController.watchEvent( + "STREAM", + streamEvent => streamEvent.StreamStatus === "SENTCONNECT", + function (streamEvent) { + 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; + }); + } + }); + } + }); +}; + +// ## User interface + +// __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) { + if (typeof(countryCode) === "undefined") return ""; + try { + return regionBundle.GetStringFromName(countryCode.toLowerCase()); + } catch (e) { + return countryCode.toUpperCase(); + } +}; + +// __showCircuitDisplay(show)__. +// If show === true, makes the circuit display visible. +let showCircuitDisplay = function (show) { + document.querySelector("svg#tor-circuit").style.display = show ? + 'block' : 'none'; +}; + // __nodeLines(nodeData)__. // Takes a nodeData array of three items each like // `{ ip : "12.34.56.78", country : "fr" }` @@ -121,7 +172,6 @@ let updateCircuitDisplay = function () { 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"), - //diagramCircles = document.querySelectorAll("svg#tor-circuit .node-circule"), lines = nodeLines(nodeData); for (let i = 0; i < diagramNodes.length; ++i) { let line = lines[i]; @@ -130,85 +180,109 @@ let updateCircuitDisplay = function () { } } // Only show the Tor circuit if we have a domain and node data. - document.querySelector("svg#tor-circuit").style.display = (domain && nodeData) ? - 'block' : 'none'; + showCircuitDisplay(domain && nodeData); } };
-// __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); - } - } - }); -}; - -// __collectIsolationData(aController)__. -// Watches for STREAM SENTCONNECT events. When a SENTCONNECT event occurs, then -// the isolation settings (SOCKS username+password) become fixed for the -// corresponding circuit. Whenever the first stream on a new circuit is seen, -// looks up u+p and records the node data in the domainToNodeDataMap. -let collectIsolationData = function (aController) { - aController.watchEvent( - "STREAM", - streamEvent => streamEvent.StreamStatus === "SENTCONNECT", - function (streamEvent) { - if (!knownCircuitIDs[streamEvent.CircuitID]) { - logger.eclog(4, "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; - updateCircuitDisplay(); - }); - } else { - updateCircuitDisplay(); - } - }); - } - }); -}; - -// __syncDisplayWithSelectedTab()__. +// __syncDisplayWithSelectedTab(syncOn)__. // 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) { +let syncDisplayWithSelectedTab = (function() { + let listener1 = event => { updateCircuitDisplay(); }, + listener2 = { onLocationChange : function (aBrowser) { + if (aBrowser === gBrowser.selectedBrowser) { + updateCircuitDisplay(); + } + } }; + return function (syncOn) { + if (syncOn) { + // Whenever a different tab is selected, change the circuit display + // to show the circuit for that tab's domain. + gBrowser.tabContainer.addEventListener("TabSelect", listener1); + // If the currently selected tab has been sent to a new location, + // update the circuit to reflect that. + gBrowser.addTabsProgressListener(listener2); + // Get started with a correct display. updateCircuitDisplay(); + } else { + // Stop syncing. + gBrowser.tabContainer.removeEventListener("TabSelect", listener1); + gBrowser.removeTabsProgressListener(listener2); + // Hide the display. + showCircuitDisplay(false); } - } }); + }; +})(); + +// ## Pref utils
- // Get started with a correct display. - updateCircuitDisplay(); +// __prefs__. A shortcut to Mozilla Services.prefs. +let prefs = Services.prefs; + +// __getPrefValue(prefName)__ +// Returns the current value of a preference, regardless of its type. +let getPrefValue = function (prefName) { + switch(prefs.getPrefType(prefName)) { + case prefs.PREF_BOOL: return prefs.getBoolPref(prefName); + case prefs.PREF_INT: return prefs.getIntPref(prefName); + case prefs.PREF_STRING: return prefs.getCharPref(prefName); + default: return null; + } };
-// __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(); - collectIsolationData(myController); +// __bindPrefAndInit(prefName, prefHandler)__ +// Applies prefHandler to the current value of pref specified by prefName. +// Re-applies prefHandler whenever the value of the pref changes. +// Returns a zero-arg function that unbinds the pref. +let bindPrefAndInit = function (prefName, prefHandler) { + let update = () => { prefHandler(getPrefValue(prefName)); }, + observer = { observe : function (subject, topic, data) { + if (data === prefName) { + update(); + } + } }; + prefs.addObserver(prefName, observer, false); + update(); + return () => { prefs.removeObserver(prefName, observer); }; };
-return display; +// ## Main function
-// Finish runTorCircuitDisplay() -})(); +// setupDisplay(host, port, password, enablePrefName)__. +// Returns a function that lets you start/stop automatic display of the Tor circuit. +// A reference to this function (called createTorCircuitDisplay) is exported as a global. +let setupDisplay = function (host, port, password, enablePrefName) { + let myController = null, + stop = function() { + if (myController) { + syncDisplayWithSelectedTab(false); + myController.close(); + myController = null; + } + }, + start = function () { + if (!myController) { + myController = controller(host, port || 9151, password, function (err) { + // An error has occurred. + logger.eclog(5, err); + logger.eclog(5, "Disabling tor display circuit because of an error."); + stop(); + }); + syncDisplayWithSelectedTab(true); + collectIsolationData(myController); + } + }; + try { + let unbindPref = bindPrefAndInit(enablePrefName, on => { if (on) start(); else stop(); }); + // When this chrome window is unloaded, we need to unbind the pref. + window.addEventListener("unload", unbindPref); + } catch (e) { + logger.eclog(5, "Error: " + e.message + "\n" + e.stack); + } +}; + +return setupDisplay;
+// Finish createTorCircuitDisplay() +})(); diff --git a/src/chrome/content/torbutton.js b/src/chrome/content/torbutton.js index 2e921da..8dba1fd 100644 --- a/src/chrome/content/torbutton.js +++ b/src/chrome/content/torbutton.js @@ -619,7 +619,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); + createTorCircuitDisplay(m_tb_control_host, m_tb_control_port, m_tb_control_pass, + "extensions.torbutton.display_circuit");
torbutton_log(3, 'init completed'); } diff --git a/src/defaults/preferences/preferences.js b/src/defaults/preferences/preferences.js index d527d7e..3e02b77 100644 --- a/src/defaults/preferences/preferences.js +++ b/src/defaults/preferences/preferences.js @@ -4,6 +4,7 @@ pref("extensions.torbutton.loglevel",4); pref("extensions.torbutton.logmethod",1); // 0=stdout, 1=errorconsole, 2=debuglog
// Display prefs +pref("extensions.torbutton.display_circuit", true); pref("extensions.torbutton.display_panel",true); pref("extensions.torbutton.panel_style",'text'); pref("extensions.torbutton@torproject.org.description", "chrome://torbutton/locale/torbutton.properties"); diff --git a/src/modules/tor-control-port.js b/src/modules/tor-control-port.js index ada80b5..eae62f5 100644 --- a/src/modules/tor-control-port.js +++ b/src/modules/tor-control-port.js @@ -156,15 +156,16 @@ io.onLineFromOnMessage = function (onMessage) { // 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)))) { + if (line.match(/^\d\d\d /) && + (pendingLines.length === 1 || + pendingLines[0].substring(0,3) === line.substring(0,3))) { // Combine pending lines to form message. let message = pendingLines.join("\r\n"); + log("controlPort >> " + message); // Wipe pendingLines before we call onMessage, in case onMessage throws an error. pendingLines = []; // Pass multiline message to onMessage. onMessage(message); - log("controlPort >> " + message); } }; }; @@ -547,11 +548,17 @@ event.watchEvent = function (controlSocket, type, filter, onData) { // Things related to the main controller. let tor = tor || {};
+// __tor.controllerCache__. +// A map from "host:port" to controller objects. Prevents redundant instantiation +// of control sockets. +tor.controllerCache = {}; + // __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); + 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); @@ -559,14 +566,11 @@ tor.controller = function (host, port, password, onError) { watchEvent : function (type, filter, onData) { event.watchEvent(socket, type, filter, onData); }, - close : socket.close }; + isOpen : () => isOpen, + close : () => { isOpen = false; 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)__. @@ -584,9 +588,12 @@ tor.controllerCache = {}; // // 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)); + let dest = host + ":" + port, + maybeController = tor.controllerCache[dest]; + return (tor.controllerCache[dest] = + (maybeController && maybeController.isOpen()) ? + maybeController : + tor.controller(host, port, password, onError)); };
// Export the controller function for external use.