commit f4980a6c647ea780eacc922846e81c0d992cbd13 Author: Kathy Brade brade@pearlcrescent.com Date: Tue Oct 11 15:37:47 2016 -0400
Bug 20111: use Unix domain sockets for SOCKS port by default
Retrieve SOCKS port configuration from Tor Launcher if available. Support the TOR_SOCKS_IPC_PATH environment variable.
Consistently use IPC to refer to Unix domain sockets.
Enhance torbutton_local_tor_check() to correctly handle spaces and escaped characters within the 'GETINFO net/listeners/socks' response.
Within startup-observer.js, use Services.jsm, Cc and similar constants, and remove unused code. --- src/chrome/content/tor-circuit-display.js | 12 +-- src/chrome/content/torbutton.js | 64 +++++++++------- src/components/startup-observer.js | 117 +++++++++++++++++++----------- src/modules/tor-control-port.js | 51 +++++++------ src/modules/utils.js | 101 +++++++++++++++++++++++++- 5 files changed, 242 insertions(+), 103 deletions(-)
diff --git a/src/chrome/content/tor-circuit-display.js b/src/chrome/content/tor-circuit-display.js index f1cf2aa..c58c6d7 100644 --- a/src/chrome/content/tor-circuit-display.js +++ b/src/chrome/content/tor-circuit-display.js @@ -6,7 +6,7 @@ // with docco.js to produce pretty documentation. // // This script is to be embedded in torbutton.xul. It defines a single global -// function, createTorCircuitDisplay(socketFile, host, port, password), which +// function, createTorCircuitDisplay(ipcFile, host, port, password), which // activates the automatic Tor circuit display for the current tab and any // future tabs. // @@ -16,9 +16,9 @@ /* global document, gBrowser, Components */
// ### Main function -// __createTorCircuitDisplay(socketFile, host, port, password, enablePrefName)__. +// __createTorCircuitDisplay(ipcFile, host, port, password, enablePrefName)__. // The single function that prepares tor circuit display. Connects to a tor -// control port with the given socketFile or host plus port, and password, and +// control port with the given ipcFile or host plus port, and password, and // binds to a named bool pref whose value determines whether the circuit display // is enabled or disabled. let createTorCircuitDisplay = (function () { @@ -312,11 +312,11 @@ let syncDisplayWithSelectedTab = (function() {
// ## Main function
-// __setupDisplay(socketFile, host, port, password, enablePrefName)__. +// __setupDisplay(ipcFile, host, port, password, enablePrefName)__. // Once called, the Tor circuit display will be started whenever // the "enablePref" is set to true, and stopped when it is set to false. // A reference to this function (called createTorCircuitDisplay) is exported as a global. -let setupDisplay = function (socketFile, host, port, password, enablePrefName) { +let setupDisplay = function (ipcFile, host, port, password, enablePrefName) { let myController = null, stopCollectingIsolationData = null, stop = function() { @@ -330,7 +330,7 @@ let setupDisplay = function (socketFile, host, port, password, enablePrefName) { }, start = function () { if (!myController) { - myController = controller(socketFile, host, port || 9151, password, + myController = controller(ipcFile, host, port || 9151, password, function (err) { // An error has occurred. logger.eclog(5, err); diff --git a/src/chrome/content/torbutton.js b/src/chrome/content/torbutton.js index d2824cb..9a8b9e2 100644 --- a/src/chrome/content/torbutton.js +++ b/src/chrome/content/torbutton.js @@ -10,6 +10,7 @@ let { LoadContextInfo } = Cu.import('resource://gre/modules/LoadContextInfo.jsm'); let { Services } = Cu.import("resource://gre/modules/Services.jsm"); let { showDialog } = Cu.import("resource://torbutton/modules/utils.js"); +let { unescapeTorString } = Cu.import("resource://torbutton/modules/utils.js");
const k_tb_last_browser_version_pref = "extensions.torbutton.lastBrowserVersion"; const k_tb_browser_update_needed_pref = "extensions.torbutton.updateNeeded"; @@ -33,9 +34,9 @@ var m_tb_window_width = window.outerWidth;
var m_tb_tbb = false;
-var m_tb_control_socket_file = null; // Set if using a UNIX domain socket. -var m_tb_control_port = null; // Set if not using a socket. -var m_tb_control_host = null; // Set if not using a socket. +var m_tb_control_ipc_file = null; // Set if using IPC (UNIX domain socket). +var m_tb_control_port = null; // Set if using TCP. +var m_tb_control_host = null; // Set if using TCP. var m_tb_control_pass = null; var m_tb_control_desc = null; // For logging.
@@ -327,15 +328,15 @@ function torbutton_init() { m_tb_control_pass = tlps.TorGetPassword(false); } catch(e) {}
- // Try to get control port socket (an nsIFile) from Tor Launcher, since - // Tor Launcher knows how to handle its own preferences and how to + // Try to get the control port IPC file (an nsIFile) from Tor Launcher, + // since Tor Launcher knows how to handle its own preferences and how to // resolve relative paths. try { - m_tb_control_socket_file = tlps.TorGetControlSocketFile(); + m_tb_control_ipc_file = tlps.TorGetControlIPCFile(); } catch(e) {}
- if (m_tb_control_socket_file) { - m_tb_control_desc = m_tb_control_socket_file.path; + if (m_tb_control_ipc_file) { + m_tb_control_desc = m_tb_control_ipc_file.path; } else { if (environ.exists("TOR_CONTROL_PORT")) { m_tb_control_port = environ.get("TOR_CONTROL_PORT"); @@ -438,7 +439,7 @@ function torbutton_init() { torbutton_notify_if_update_needed(); torbutton_update_sync_ui();
- createTorCircuitDisplay(m_tb_control_socket_file, m_tb_control_host, + createTorCircuitDisplay(m_tb_control_ipc_file, m_tb_control_host, m_tb_control_port, m_tb_control_pass, "extensions.torbutton.display_circuit");
@@ -1009,8 +1010,8 @@ function torbutton_send_ctrl_cmd(command) { let sts = Cc["@mozilla.org/network/socket-transport-service;1"] .getService(Ci.nsISocketTransportService); let socket; - if (m_tb_control_socket_file) { - socket = sts.createUnixDomainTransport(m_tb_control_socket_file); + if (m_tb_control_ipc_file) { + socket = sts.createUnixDomainTransport(m_tb_control_ipc_file); } else { socket = sts.createTransport(null, 0, m_tb_control_host, m_tb_control_port, null); @@ -1350,7 +1351,7 @@ function torbutton_do_new_identity() { torbutton_log(3, "New Identity: Sending NEWNYM");
// We only support TBB for newnym. - if (!m_tb_control_pass || (!m_tb_control_socket_file && !m_tb_control_port)) { + if (!m_tb_control_pass || (!m_tb_control_ipc_file && !m_tb_control_port)) { var warning = torbutton_get_property_string("torbutton.popup.no_newnym"); torbutton_log(5, "Torbutton cannot safely newnym. It does not have access to the Tor Control Port."); window.alert(warning); @@ -1508,7 +1509,7 @@ function torbutton_do_tor_check() const kEnvUseTransparentProxy = "TOR_TRANSPROXY"; var env = Cc["@mozilla.org/process/environment;1"] .getService(Ci.nsIEnvironment); - if ((m_tb_control_socket_file || m_tb_control_port) && + if ((m_tb_control_ipc_file || m_tb_control_port) && !env.exists(kEnvUseTransparentProxy) && !env.exists(kEnvSkipControlPortTest) && m_tb_prefs.getBoolPref("extensions.torbutton.local_tor_check")) { @@ -1559,19 +1560,20 @@ function torbutton_local_tor_check() }
// Sample response: net/listeners/socks="127.0.0.1:9149" "127.0.0.1:9150" - // First, check for command argument prefix. + // First, check for and remove the command argument prefix. if (0 != resp.indexOf(kCmdArg + '=')) { logUnexpectedResponse(); return false; } + resp = resp.substr(kCmdArg.length + 1);
// Retrieve configured proxy settings and check each listener against them. - // When a Unix domain socket is configured, a file URL should be present in - // network.proxy.socks. + // When the SOCKS prefs are set to use IPC (e.g., a Unix domain socket), a + // file URL should be present in network.proxy.socks. // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1211567 let socksAddr = m_tb_prefs.getCharPref("network.proxy.socks"); let socksPort = m_tb_prefs.getIntPref("network.proxy.socks_port"); - let socketPath; + let socksIPCPath; if (socksAddr && socksAddr.startsWith("file:")) { // Convert the file URL to a file path. try { @@ -1579,19 +1581,29 @@ function torbutton_local_tor_check() .getService(Ci.nsIIOService); let fph = ioService.getProtocolHandler("file") .QueryInterface(Ci.nsIFileProtocolHandler); - socketPath = fph.getFileFromURLSpec(socksAddr).path; + socksIPCPath = fph.getFileFromURLSpec(socksAddr).path; } catch (e) { - torbutton_log(5, "Local Tor check: Unix domain socket error: " + e); + torbutton_log(5, "Local Tor check: IPC file error: " + e); return false; } } else { socksAddr = removeBrackets(socksAddr); }
- let addrArray = resp.substr(kCmdArg.length + 1).split(' '); + // Split into quoted strings. This code is adapted from utils.splitAtSpaces() + // within tor-control-port.js; someday this code should use the entire + // tor-control-port.js framework. + let addrArray = []; + resp.replace(/((\S*?"(.*?)")+\S*|\S+)/g, function (a, captured) { + addrArray.push(captured); + }); + let foundSocksListener = false; for (let i = 0; !foundSocksListener && (i < addrArray.length); ++i) { - let addr = addrArray[i]; + let addr; + try { addr = unescapeTorString(addrArray[i]); } catch (e) {} + if (!addr) + continue;
// Remove double quotes if present. let len = addr.length; @@ -1599,14 +1611,14 @@ function torbutton_local_tor_check() addr = addr.substring(1, len - 1);
if (addr.startsWith("unix:")) { - if (!socketPath) + if (!socksIPCPath) continue;
// Check against the configured UNIX domain socket proxy. let path = addr.substring(5); - torbutton_log(2, "Tor socks listener (socket): " + path); - foundSocksListener = (socketPath === path); - } else if (!socketPath) { + torbutton_log(2, "Tor socks listener (Unix domain socket): " + path); + foundSocksListener = (socksIPCPath === path); + } else if (!socksIPCPath) { // Check against the configured TCP proxy. We expect addr:port where addr // may be an IPv6 address; that is, it may contain colon characters. // Also, we remove enclosing square brackets before comparing addresses @@ -2088,7 +2100,7 @@ function torbutton_check_protections() // See https://trac.torproject.org/projects/tor/ticket/10353 for more info. document.getElementById("torbutton-cookie-protector").hidden = m_tb_prefs.getBoolPref("browser.privatebrowsing.autostart");
- if (!m_tb_control_pass || (!m_tb_control_socket_file && !m_tb_control_port)) { + if (!m_tb_control_pass || (!m_tb_control_ipc_file && !m_tb_control_port)) { document.getElementById("torbutton-new-identity").disabled = true; }
diff --git a/src/components/startup-observer.js b/src/components/startup-observer.js index f083482..6698b0b 100644 --- a/src/components/startup-observer.js +++ b/src/components/startup-observer.js @@ -15,6 +15,13 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "FileUtils", + "resource://gre/modules/FileUtils.jsm"); +
// Module specific constants const kMODULE_NAME = "Startup"; @@ -22,10 +29,9 @@ const kMODULE_CONTRACTID = "@torproject.org/startup-observer;1"; const kMODULE_CID = Components.ID("06322def-6fde-4c06-aef6-47ae8e799629");
function StartupObserver() { - this.logger = Components.classes["@torproject.org/torbutton-logger;1"] - .getService(Components.interfaces.nsISupports).wrappedJSObject; - this._prefs = Components.classes["@mozilla.org/preferences-service;1"] - .getService(Components.interfaces.nsIPrefBranch); + this.logger = Cc["@torproject.org/torbutton-logger;1"] + .getService(Ci.nsISupports).wrappedJSObject; + this._prefs = Services.prefs; this.logger.log(3, "Startup Observer created");
var env = Cc["@mozilla.org/process/environment;1"] @@ -61,41 +67,74 @@ StartupObserver.prototype = { // some weird proxy caching code that showed up in FF15. // Otherwise, homepage domain loads fail forever. setProxySettings: function() { + if (!this.is_tbb) + return; + // Bug 1506: Still want to get these env vars - var environ = Components.classes["@mozilla.org/process/environment;1"] - .getService(Components.interfaces.nsIEnvironment); - - if (environ.exists("TOR_SOCKS_PORT")) { - if (this.is_tbb) { - this._prefs.setIntPref('network.proxy.socks_port', - parseInt(environ.get("TOR_SOCKS_PORT"))); - this._prefs.setBoolPref('network.proxy.socks_remote_dns', true); - this._prefs.setIntPref('network.proxy.type', 1); + let environ = Cc["@mozilla.org/process/environment;1"] + .getService(Ci.nsIEnvironment); + if (environ.exists("TOR_TRANSPROXY")) { + this.logger.log(3, "Resetting Tor settings to transproxy"); + this._prefs.setBoolPref("network.proxy.socks_remote_dns", false); + this._prefs.setIntPref("network.proxy.type", 0); + this._prefs.setIntPref("network.proxy.socks_port", 0); + this._prefs.setCharPref("network.proxy.socks", ""); + } else { + // Try to retrieve SOCKS proxy settings from Tor Launcher. + let socksPortInfo; + try { + let tlps = Cc["@torproject.org/torlauncher-protocol-service;1"] + .getService(Ci.nsISupports).wrappedJSObject; + socksPortInfo = tlps.TorGetSOCKSPortInfo(); + } catch(e) {} + + // If Tor Launcher is not available, check environment variables. + if (!socksPortInfo) { + socksPortInfo = { ipcFile: undefined, host: undefined, port: 0 }; + + let isWindows = Services.appinfo.OS === "WINNT"; + if (!isWindows && environ.exists("TOR_SOCKS_IPC_PATH")) { + socksPortInfo.ipcFile = new FileUtils.File( + environ.get("TOR_SOCKS_IPC_PATH")); + } + else + { + if (environ.exists("TOR_SOCKS_HOST")) + socksPortInfo.host = environ.get("TOR_SOCKS_HOST"); + if (environ.exists("TOR_SOCKS_PORT")) + socksPortInfo.port = parseInt(environ.get("TOR_SOCKS_PORT")); + } } - this.logger.log(3, "Reset socks port to "+environ.get("TOR_SOCKS_PORT")); - }
- if (environ.exists("TOR_SOCKS_HOST")) { - if (this.is_tbb) { - this._prefs.setCharPref('network.proxy.socks', environ.get("TOR_SOCKS_HOST")); + // Adjust network.proxy prefs. + if (socksPortInfo.ipcFile) { + let fph = Services.io.getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + let fileURI = fph.newFileURI(socksPortInfo.ipcFile); + this.logger.log(3, "Reset socks to "+fileURI.spec); + this._prefs.setCharPref("network.proxy.socks", fileURI.spec); + this._prefs.setIntPref("network.proxy.socks_port", 0); + } else { + if (socksPortInfo.host) { + this._prefs.setCharPref("network.proxy.socks", socksPortInfo.host); + this.logger.log(3, "Reset socks host to "+socksPortInfo.host); + } + if (socksPortInfo.port) { + this._prefs.setIntPref("network.proxy.socks_port", + socksPortInfo.port); + this.logger.log(3, "Reset socks port to "+socksPortInfo.port); + } } - }
- if (environ.exists("TOR_TRANSPROXY")) { - this.logger.log(3, "Resetting Tor settings to transproxy"); - if (this.is_tbb) { - this._prefs.setBoolPref('network.proxy.socks_remote_dns', false); - this._prefs.setIntPref('network.proxy.type', 0); - this._prefs.setIntPref('network.proxy.socks_port', 0); - this._prefs.setCharPref('network.proxy.socks', ""); + if (socksPortInfo.ipcFile || socksPortInfo.host || socksPortInfo.port) { + this._prefs.setBoolPref("network.proxy.socks_remote_dns", true); + this._prefs.setIntPref("network.proxy.type", 1); } }
// Force prefs to be synced to disk - var prefService = Components.classes["@mozilla.org/preferences-service;1"] - .getService(Components.interfaces.nsIPrefService); - prefService.savePrefFile(null); - + this._prefs.savePrefFile(null); + this.logger.log(3, "Synced network settings to environment."); },
@@ -109,16 +148,14 @@ StartupObserver.prototype = { }
// In all cases, force prefs to be synced to disk - var prefService = Components.classes["@mozilla.org/preferences-service;1"] - .getService(Components.interfaces.nsIPrefService); - prefService.savePrefFile(null); + this._prefs.savePrefFile(null); },
QueryInterface: function(iid) { - if (iid.equals(Components.interfaces.nsISupports)) { + if (iid.equals(Ci.nsISupports)) { return this; } - if(iid.equals(Components.interfaces.nsIClassInfo)) { + if(iid.equals(Ci.nsIClassInfo)) { return this; } return this; @@ -141,12 +178,4 @@ StartupObserver.prototype = {
};
-/** -* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4). -* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6). -*/ -Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); -if (XPCOMUtils.generateNSGetFactory) - var NSGetFactory = XPCOMUtils.generateNSGetFactory([StartupObserver]); -else - var NSGetModule = XPCOMUtils.generateNSGetModule([StartupObserver]); +var NSGetFactory = XPCOMUtils.generateNSGetFactory([StartupObserver]); diff --git a/src/modules/tor-control-port.js b/src/modules/tor-control-port.js index f23daed..a7f0434 100644 --- a/src/modules/tor-control-port.js +++ b/src/modules/tor-control-port.js @@ -10,7 +10,7 @@ // let { controller } = Components.utils.import("path/to/tor-control-port.js"); // // See the last function defined in this file: -// controller(socketFile, host, port, password, onError) +// controller(ipcFile, host, port, password, onError) // for usage of the controller function.
/* jshint esnext: true */ @@ -42,18 +42,18 @@ log("Loading tor-control-port.js\n"); // I/O utilities namespace let io = {};
-// __io.asyncSocketStreams(socketFile, host, port)__. +// __io.asyncSocketStreams(ipcFile, host, port)__. // Creates a pair of asynchronous input and output streams for a socket at the -// given socketFile or host and port. -io.asyncSocketStreams = function (socketFile, host, port) { +// given ipcFile or host and port. +io.asyncSocketStreams = function (ipcFile, host, port) { let sts = Cc["@mozilla.org/network/socket-transport-service;1"] .getService(Components.interfaces.nsISocketTransportService), UNBUFFERED = Ci.nsITransport.OPEN_UNBUFFERED;
// Create an instance of a socket transport. let socketTransport; - if (socketFile) { - socketTransport = sts.createUnixDomainTransport(socketFile); + if (ipcFile) { + socketTransport = sts.createUnixDomainTransport(ipcFile); } else { socketTransport = sts.createTransport(null, 0, host, port, null); } @@ -99,16 +99,15 @@ io.pumpInputStream = function (inputStream, onInputData, onError) { } }, null); };
-// __io.asyncSocket(socketFile, host, port, onInputData, onError)__. -// Creates an asynchronous, text-oriented UNIX domain socket (if socketFile -// is defined) or TCP socket at host:port. +// __io.asyncSocket(ipcFile, host, port, onInputData, onError)__. +// Creates an asynchronous, text-oriented IPC socket (if ipcFile is defined) +// or a 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 (socketFile, host, port, onInputData, onError) { - let [inputStream, outputStream] = io.asyncSocketStreams(socketFile, host, - port), +io.asyncSocket = function (ipcFile, host, port, onInputData, onError) { + let [inputStream, outputStream] = io.asyncSocketStreams(ipcFile, host, port), pendingWrites = []; // Run an input stream pump to send incoming data to the onInputData callback. io.pumpInputStream(inputStream, onInputData, onError); @@ -253,8 +252,8 @@ io.matchRepliesToCommands = function (asyncSend, dispatcher) { }); };
-// __io.controlSocket(socketFile, host, port, password, onError)__. -// Instantiates and returns a socket to a tor ControlPort at socketFile or +// __io.controlSocket(ipcFile, host, port, password, onError)__. +// Instantiates and returns a socket to a tor ControlPort at ipcFile or // host:port, authenticating with the given password. onError is called with an // error object as its single argument whenever an error occurs. Example: // @@ -269,11 +268,11 @@ io.matchRepliesToCommands = function (asyncSend, dispatcher) { // socket.removeNotificationCallback(callback); // // Close the socket permanently // socket.close(); -io.controlSocket = function (socketFile, host, port, password, onError) { +io.controlSocket = function (ipcFile, host, port, password, onError) { // Produce a callback dispatcher for Tor messages. let mainDispatcher = io.callbackDispatcher(), // Open the socket and convert format to Tor messages. - socket = io.asyncSocket(socketFile, host, port, + socket = io.asyncSocket(ipcFile, host, port, io.onDataFromOnLine( io.onLineFromOnMessage(mainDispatcher.pushMessage)), onError), @@ -620,12 +619,12 @@ let tor = {}; // redundant instantiation of control sockets. tor.controllerCache = {};
-// __tor.controller(socketFile, host, port, password, onError)__. -// Creates a tor controller at the given socketFile or host and port, with the +// __tor.controller(ipcFile, host, port, password, onError)__. +// Creates a tor controller at the given ipcFile or host and port, with the // given password. // onError returns asynchronously whenever a connection error occurs. -tor.controller = function (socketFile, host, port, password, onError) { - let socket = io.controlSocket(socketFile, host, port, password, onError), +tor.controller = function (ipcFile, host, port, password, onError) { + let socket = io.controlSocket(ipcFile, host, port, password, onError), isOpen = true; return { getInfo : key => info.getInfo(socket, key), getConf : key => info.getConf(socket, key), @@ -638,11 +637,11 @@ tor.controller = function (socketFile, host, port, password, onError) {
// ## Export
-// __controller(socketFile, host, port, password, onError)__. +// __controller(ipcFile, host, port, password, onError)__. // Instantiates and returns a controller object connected to a tor ControlPort -// on socketFile or at host:port, authenticating with the given password, if +// on ipcFile or at host:port, authenticating with the given password, if // the controller doesn't yet exist. Otherwise returns the existing controller -// to the given socketFile or host:port. +// to the given ipcFile or host:port. // onError is called with an error object as its single argument whenever // an error occurs. Example: // @@ -653,13 +652,13 @@ tor.controller = function (socketFile, host, port, password, onError) { // let replyPromise = c.getInfo("ip-to-country/16.16.16.16"); // // Close the controller permanently // c.close(); -var controller = function (socketFile, host, port, password, onError) { - let dest = (socketFile) ? "unix:" + socketFile.path : host + ":" + port, +var controller = function (ipcFile, host, port, password, onError) { + let dest = (ipcFile) ? "unix:" + ipcFile.path : host + ":" + port, maybeController = tor.controllerCache[dest]; return (tor.controllerCache[dest] = (maybeController && maybeController.isOpen()) ? maybeController : - tor.controller(socketFile, host, port, password, onError)); + tor.controller(ipcFile, host, port, password, onError)); };
// Export the controller function for external use. diff --git a/src/modules/utils.js b/src/modules/utils.js index 514ef51..b303485 100644 --- a/src/modules/utils.js +++ b/src/modules/utils.js @@ -74,5 +74,104 @@ var showDialog = function (parent, url, name, features) { } };
+// ## Tor control protocol utility functions + +let _torControl = { + // Unescape Tor Control string aStr (removing surrounding "" and \ escapes). + // Based on Vidalia's src/common/stringutil.cpp:string_unescape(). + // Returns the unescaped string. Throws upon failure. + // Within Tor Launcher, the file components/tl-protocol.js also contains a + // copy of _strUnescape(). + _strUnescape: function(aStr) + { + if (!aStr) + return aStr; + + var len = aStr.length; + if ((len < 2) || ('"' != aStr.charAt(0)) || ('"' != aStr.charAt(len - 1))) + return aStr; + + const kHexRE = /[0-9A-Fa-f]{2}/; + const kOctalRE = /[0-7]{3}/; + var rv = ""; + var i = 1; + var lastCharIndex = len - 2; + while (i <= lastCharIndex) + { + var c = aStr.charAt(i); + if ('\' == c) + { + if (++i > lastCharIndex) + throw new Error("missing character after \"); + + c = aStr.charAt(i); + if ('n' == c) + rv += '\n'; + else if ('r' == c) + rv += '\r'; + else if ('t' == c) + rv += '\t'; + else if ('x' == c) + { + if ((i + 2) > lastCharIndex) + throw new Error("not enough hex characters"); + + let s = aStr.substr(i + 1, 2); + if (!kHexRE.test(s)) + throw new Error("invalid hex characters"); + + let val = parseInt(s, 16); + rv += String.fromCharCode(val); + i += 3; + } + else if (this._isDigit(c)) + { + let s = aStr.substr(i, 3); + if ((i + 2) > lastCharIndex) + throw new Error("not enough octal characters"); + + if (!kOctalRE.test(s)) + throw new Error("invalid octal characters"); + + let val = parseInt(s, 8); + rv += String.fromCharCode(val); + i += 3; + } + else // "\" and others + { + rv += c; + ++i; + } + } + else if ('"' == c) + throw new Error("unescaped " within string"); + else + { + rv += c; + ++i; + } + } + + // Convert from UTF-8 to Unicode. TODO: is UTF-8 always used in protocol? + return decodeURIComponent(escape(rv)); + }, // _strUnescape() + + // Within Tor Launcher, the file components/tl-protocol.js also contains a + // copy of _isDigit(). + _isDigit: function(aChar) + { + const kRE = /^\d$/; + return aChar && kRE.test(aChar); + }, +}; // _torControl + +// __unescapeTorString(str, resultObj)__. +// Unescape Tor Control string str (removing surrounding "" and \ escapes). +// Returns the unescaped string. Throws upon failure. +var unescapeTorString = function(str) { + return _torControl._strUnescape(str); +}; + // Export utility functions for external use. -let EXPORTED_SYMBOLS = ["bindPrefAndInit", "getPrefValue", "getEnv", "showDialog"]; +let EXPORTED_SYMBOLS = ["bindPrefAndInit", "getPrefValue", "getEnv", + "showDialog", "unescapeTorString"];