[tor-commits] [arm/master] Adding tor control socket support

atagar at torproject.org atagar at torproject.org
Wed Aug 10 17:03:30 UTC 2011


commit cca2aae001bab0e6bb09e81a4e9df11bec2bc2bb
Author: Damian Johnson <atagar at torproject.org>
Date:   Wed Aug 10 09:55:13 2011 -0700

    Adding tor control socket support
    
    This adds support for connecting via a tor control socket. Connection behavior
    is as follows...
    
    - if an argument or armrc option for the control port or socket path are
      provided then just use that connection method
    - if neither or both args are provided then the default behavior is...
      - try connecting to the control socket (defaulting to /var/lib/tor/control)
      - try connecting to the control ports (defaulting to 9051 and 9052)
      - fall back to starting arm without a tor instance and show setup wizard
    
    Control socket support is a feature request by Dererk on...
    https://trac.torproject.org/projects/tor/ticket/3638
---
 armrc.sample           |    5 ++-
 src/cli/headerPanel.py |   55 +++++++++++++++++---------
 src/starter.py         |  100 +++++++++++++++++++++++++++++++++++-------------
 src/util/torTools.py   |   22 ++++++++++
 4 files changed, 135 insertions(+), 47 deletions(-)

diff --git a/armrc.sample b/armrc.sample
index d547b6c..22fb903 100644
--- a/armrc.sample
+++ b/armrc.sample
@@ -2,6 +2,7 @@
 startup.controlPassword
 startup.interface.ipAddress 127.0.0.1
 startup.interface.port 9051
+startup.interface.socket /var/lib/tor/control
 startup.blindModeEnabled false
 startup.events N3
 startup.dataDirectory ~/.arm
@@ -66,7 +67,9 @@ features.refreshRate 5
 features.confirmQuit true
 
 # Allows arm to start when there's no running tor instance if true, otherwise
-# we terminate right away.
+# we terminate right away. This is ignored if the user provides an option
+# specifying how to connect to tor (ie, a 'startup.interface.*' option or
+# startup argument).
 features.allowDetachedStartup true
 
 # Paremters for the log panel
diff --git a/src/cli/headerPanel.py b/src/cli/headerPanel.py
index 4e78f6c..d5ce332 100644
--- a/src/cli/headerPanel.py
+++ b/src/cli/headerPanel.py
@@ -21,6 +21,7 @@ import threading
 
 import TorCtl.TorCtl
 
+import starter
 import cli.popups
 import cli.controller
 
@@ -40,6 +41,7 @@ VERSION_STATUS_COLORS = {"new": "blue", "new in series": "blue", "obsolete": "re
 
 DEFAULT_CONFIG = {"startup.interface.ipAddress": "127.0.0.1",
                   "startup.interface.port": 9051,
+                  "startup.interface.socket": "/var/lib/tor/control",
                   "features.showFdUsage": False,
                   "log.fdUsageSixtyPercent": log.NOTICE,
                   "log.fdUsageNinetyPercent": log.WARN}
@@ -132,27 +134,42 @@ class HeaderPanel(panel.Panel, threading.Thread):
     if key in (ord('n'), ord('N')) and torTools.getConn().isNewnymAvailable():
       self.sendNewnym()
     elif key in (ord('r'), ord('R')) and not self._isTorConnected:
-      try:
-        ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"]
-        tmpConn, authType, authValue = TorCtl.TorCtl.preauth_connect(ctlAddr, ctlPort)
-        
-        if authType == TorCtl.TorCtl.AUTH_TYPE.PASSWORD:
-          authValue = cli.popups.inputPrompt("Controller Password: ")
-          if not authValue: raise IOError() # cancel reconnection
-        
-        tmpConn.authenticate(authValue)
-        torTools.getConn().init(tmpConn)
+      torctlConn = None
+      allowPortConnection, allowSocketConnection, _ = starter.allowConnectionTypes()
+      
+      if os.path.exists(self._config["startup.interface.socket"]) and allowSocketConnection:
+        try: torctlConn = torTools.connect_socket(self._config["startup.interface.socket"])
+        except IOError, exc:
+          if not allowPortConnection:
+            cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
+      elif not allowPortConnection:
+        cli.popups.showMsg("Unable to reconnect (socket '%s' doesn't exist)" % self._config["startup.interface.socket"], 3)
+      
+      if not torctlConn and allowPortConnection:
+        try:
+          ctlAddr, ctlPort = self._config["startup.interface.ipAddress"], self._config["startup.interface.port"]
+          tmpConn, authType, authValue = TorCtl.TorCtl.preauth_connect(ctlAddr, ctlPort)
+          
+          if authType == TorCtl.TorCtl.AUTH_TYPE.PASSWORD:
+            authValue = cli.popups.inputPrompt("Controller Password: ")
+            if not authValue: raise IOError() # cancel reconnection
+          
+          tmpConn.authenticate(authValue)
+          torctlConn = tmpConn
+        except Exception, exc:
+          # attempts to use the wizard port too
+          try:
+            cli.controller.getController().getTorManager().connectManagedInstance()
+            log.log(log.NOTICE, "Reconnected to Tor's control port")
+            cli.popups.showMsg("Tor reconnected", 1)
+          except:
+            # displays notice for the first failed connection attempt
+            if exc.args: cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
+      
+      if torctlConn:
+        torTools.getConn().init(torctlConn)
         log.log(log.NOTICE, "Reconnected to Tor's control port")
         cli.popups.showMsg("Tor reconnected", 1)
-      except Exception, exc:
-        # attempts to use the wizard port too
-        try:
-          cli.controller.getController().getTorManager().connectManagedInstance()
-          log.log(log.NOTICE, "Reconnected to Tor's control port")
-          cli.popups.showMsg("Tor reconnected", 1)
-        except:
-          # displays notice for the first failed connection attempt
-          if exc.args: cli.popups.showMsg("Unable to reconnect (%s)" % exc, 3)
     else: isKeystrokeConsumed = False
     
     return isKeystrokeConsumed
diff --git a/src/starter.py b/src/starter.py
index 01acf24..a148947 100644
--- a/src/starter.py
+++ b/src/starter.py
@@ -35,6 +35,7 @@ DEFAULT_CONFIG = os.path.expanduser("~/.arm/armrc")
 CONFIG = {"startup.controlPassword": None,
           "startup.interface.ipAddress": "127.0.0.1",
           "startup.interface.port": 9051,
+          "startup.interface.socket": "/var/lib/tor/control",
           "startup.blindModeEnabled": False,
           "startup.events": "N3",
           "startup.dataDirectory": "~/.arm",
@@ -52,14 +53,16 @@ CONFIG = {"startup.controlPassword": None,
           "log.configDescriptions.persistance.saveFailed": util.log.NOTICE,
           "log.savingDebugLog": util.log.NOTICE}
 
-OPT = "gi:c:dbe:vh"
-OPT_EXPANDED = ["gui", "interface=", "config=", "debug", "blind", "event=", "version", "help"]
+OPT = "gi:s:c:dbe:vh"
+OPT_EXPANDED = ["gui", "interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"]
 
 HELP_MSG = """Usage arm [OPTION]
 Terminal status monitor for Tor relays.
 
   -g, --gui                       launch the Gtk+ interface
   -i, --interface [ADDRESS:]PORT  change control interface from %s:%i
+  -s, --socket SOCKET_PATH        attach using unix domain socket if present,
+                                    SOCKET_PATH defaults to: %s
   -c, --config CONFIG_PATH        loaded configuration options, CONFIG_PATH
                                     defaults to: %s
   -d, --debug                     writes all arm logs to %s
@@ -72,7 +75,7 @@ Terminal status monitor for Tor relays.
 Example:
 arm -b -i 1643          hide connection data, attaching to control port 1643
 arm -e we -c /tmp/cfg   use this configuration file with 'WARN'/'ERR' events
-""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], cli.logPanel.EVENT_LISTING)
+""" % (CONFIG["startup.interface.ipAddress"], CONFIG["startup.interface.port"], CONFIG["startup.interface.socket"], DEFAULT_CONFIG, LOG_DUMP_PATH, CONFIG["startup.events"], cli.logPanel.EVENT_LISTING)
 
 # filename used for cached tor config descriptions
 CONFIG_DESC_FILENAME = "torConfigDesc.txt"
@@ -94,6 +97,29 @@ STANDARD_CFG_NOT_FOUND_MSG = "No configuration found at '%s', using defaults"
 # torrc entries that are scrubbed when dumping
 PRIVATE_TORRC_ENTRIES = ["HashedControlPassword", "Bridge", "HiddenServiceDir"]
 
+def allowConnectionTypes():
+  """
+  This provides a tuple with booleans indicating if we should or shouldn't
+  attempt to connect by various methods...
+  (allowPortConnection, allowSocketConnection, allowDetachedStart)
+  """
+  
+  confKeys = util.conf.getConfig("arm").getKeys()
+  
+  isPortArgPresent = "startup.interface.ipAddress" in confKeys or "startup.interface.port" in confKeys
+  isSocketArgPresent = "startup.interface.socket" in confKeys
+  
+  skipPortConnection = isSocketArgPresent and not isPortArgPresent
+  skipSocketConnection = isPortArgPresent and not isSocketArgPresent
+  
+  # Flag to indicate if we'll start arm reguardless of being unable to connect
+  # to Tor. This is the default behavior if the user hasn't provided a port or
+  # socket to connect to, so we can show the relay setup wizard.
+  
+  allowDetachedStart = CONFIG["features.allowDetachedStartup"] and not isPortArgPresent and not isSocketArgPresent
+  
+  return (not skipPortConnection, not skipSocketConnection, allowDetachedStart)
+
 def _loadConfigurationDescriptions(pathPrefix):
   """
   Attempts to load descriptions for tor's configuration options, fetching them
@@ -168,7 +194,7 @@ def _loadConfigurationDescriptions(pathPrefix):
         msg = DESC_INTERNAL_LOAD_FAILED_MSG % util.sysTools.getFileErrorMsg(exc)
         util.log.log(CONFIG["log.configDescriptions.internalLoadFailed"], msg)
 
-def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, incorrectPasswordMsg=""):
+def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, incorrectPasswordMsg="", printError=True):
   """
   Custom handler for establishing a TorCtl connection.
   """
@@ -222,7 +248,7 @@ def _torCtlConnect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None, i
       # again prompting for the user to enter it
       print incorrectPasswordMsg
       return _torCtlConnect(controlAddr, controlPort)
-    elif not CONFIG["features.allowDetachedStartup"]:
+    elif printError:
       print exc
       return None
 
@@ -300,6 +326,8 @@ if __name__ == '__main__':
       
       param["startup.interface.ipAddress"] = controlAddr
       param["startup.interface.port"] = controlPort
+    elif opt in ("-s", "--socket"):
+      param["startup.interface.socket"] = arg
     elif opt in ("-g", "--gui"): launchGui = True
     elif opt in ("-c", "--config"): configPath = arg  # sets path of user's config
     elif opt in ("-d", "--debug"): isDebugMode = True # dumps all logs
@@ -361,7 +389,7 @@ if __name__ == '__main__':
   for utilModule in (util.conf, util.connections, util.hostnames, util.log, util.panel, util.procTools, util.sysTools, util.torConfig, util.torTools, util.uiTools):
     utilModule.loadConfig(config)
   
-  # snycs config and parameters, saving changed config options and overwriting
+  # syncs config and parameters, saving changed config options and overwriting
   # undefined parameters with defaults
   for key in param.keys():
     if param[key] == None: param[key] = CONFIG[key]
@@ -389,28 +417,46 @@ if __name__ == '__main__':
   # temporarily disables TorCtl logging to prevent issues from going to stdout while starting
   TorCtl.TorUtil.loglevel = "NONE"
   
-  # sets up TorCtl connection, prompting for the passphrase if necessary and
-  # sending problems to stdout if they arise
-  authPassword = config.get("startup.controlPassword", CONFIG["startup.controlPassword"])
-  incorrectPasswordMsg = "Password found in '%s' was incorrect" % configPath
-  conn = _torCtlConnect(controlAddr, controlPort, authPassword, incorrectPasswordMsg)
-  if conn == None and not CONFIG["features.allowDetachedStartup"]: sys.exit(1)
-  
-  # removing references to the controller password so the memory can be freed
-  # (unfortunately python does allow for direct access to the memory so this
-  # is the best we can do)
-  del authPassword
-  if "startup.controlPassword" in config.contents:
-    del config.contents["startup.controlPassword"]
-    
-    pwLineNum = None
-    for i in range(len(config.rawContents)):
-      if config.rawContents[i].strip().startswith("startup.controlPassword"):
-        pwLineNum = i
-        break
+  # By default attempts to connect using the control socket if it exists. This
+  # skips attempting to connect by socket or port if the user has given
+  # arguments for connecting to the other.
+  
+  conn = None
+  allowPortConnection, allowSocketConnection, allowDetachedStart = allowConnectionTypes()
+  
+  socketPath = param["startup.interface.socket"]
+  if os.path.exists(socketPath) and allowSocketConnection:
+    try: conn = util.torTools.connect_socket(socketPath)
+    except IOError, exc:
+      if not allowPortConnection:
+        print "Unable to use socket '%s': %s" % (socketPath, exc)
+  elif not allowPortConnection:
+    print "Socket '%s' doesn't exist" % socketPath
+  
+  if not conn and allowPortConnection:
+    # sets up TorCtl connection, prompting for the passphrase if necessary and
+    # sending problems to stdout if they arise
+    authPassword = config.get("startup.controlPassword", CONFIG["startup.controlPassword"])
+    incorrectPasswordMsg = "Password found in '%s' was incorrect" % configPath
+    conn = _torCtlConnect(controlAddr, controlPort, authPassword, incorrectPasswordMsg, not allowDetachedStart)
     
-    if pwLineNum != None:
-      del config.rawContents[i]
+    # removing references to the controller password so the memory can be freed
+    # (unfortunately python does allow for direct access to the memory so this
+    # is the best we can do)
+    del authPassword
+    if "startup.controlPassword" in config.contents:
+      del config.contents["startup.controlPassword"]
+      
+      pwLineNum = None
+      for i in range(len(config.rawContents)):
+        if config.rawContents[i].strip().startswith("startup.controlPassword"):
+          pwLineNum = i
+          break
+      
+      if pwLineNum != None:
+        del config.rawContents[i]
+  
+  if conn == None and not allowDetachedStart: sys.exit(1)
   
   # initializing the connection may require user input (for the password)
   # skewing the startup time results so this isn't counted
diff --git a/src/util/torTools.py b/src/util/torTools.py
index f81e10a..366deb0 100644
--- a/src/util/torTools.py
+++ b/src/util/torTools.py
@@ -111,6 +111,28 @@ IS_STARTUP_SIGNAL = True
 def loadConfig(config):
   config.update(CONFIG)
 
+# TODO: temporary code until this is added to torctl as part of...
+# https://trac.torproject.org/projects/tor/ticket/3638
+def connect_socket(socketPath="/var/lib/tor/control", ConnClass=TorCtl.Connection):
+  """
+  Connects to a unix domain socket available to controllers (set via tor's
+  ControlSocket option). This raises an IOError if unable to do so.
+
+  Arguments:
+    socketPath - path of the socket to attach to
+    ConnClass  - connection type to instantiate
+  """
+
+  import socket
+  try:
+    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+    s.connect(socketPath)
+    conn = ConnClass(s)
+    conn.authenticate("")
+    return conn
+  except Exception, exc:
+    raise IOError(exc)
+
 def getPid(controlPort=9051, pidFilePath=None):
   """
   Attempts to determine the process id for a running tor process, using the



More information about the tor-commits mailing list