[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[or-cvs] r22234: {arm} The last batch commit (note: this is *not* a shiny, new rele (in arm/trunk: init interface util)



Author: atagar
Date: 2010-04-25 21:15:26 +0000 (Sun, 25 Apr 2010)
New Revision: 22234

Added:
   arm/trunk/util/conf.py
   arm/trunk/util/torTools.py
Modified:
   arm/trunk/init/starter.py
   arm/trunk/interface/connPanel.py
   arm/trunk/interface/controller.py
   arm/trunk/util/connections.py
Log:
The last batch commit (note: this is *not* a shiny, new release - just the parts I have done).
added: custom settings config, currently just used for the controller password (requested by ioerror)
fix: removed -p option due to being a gaping security problem (caught by ioerror and nickm)
fix: preventing the connection panel from resetting while in blind mode (caught by micah)
fix: ss resolution wasn't specifying the use of numeric ports (caught by data)
fix: crashing issue when trying to resolve addresses without network connectivity
fix: forgot to join on connection resolver when quitting



Modified: arm/trunk/init/starter.py
===================================================================
--- arm/trunk/init/starter.py	2010-04-25 19:03:47 UTC (rev 22233)
+++ arm/trunk/init/starter.py	2010-04-25 21:15:26 UTC (rev 22234)
@@ -6,31 +6,36 @@
 command line parameters.
 """
 
+import os
 import sys
-import socket
 import getopt
-import getpass
 
 # includes parent directory rather than init in path (so sibling modules are included)
 sys.path[0] = sys.path[0][:-5]
 
-from TorCtl import TorCtl, TorUtil
-from interface import controller, logPanel
+import interface.controller
+import interface.logPanel
+import util.conf
+import util.torTools
+import TorCtl.TorUtil
 
 VERSION = "1.3.5"
 LAST_MODIFIED = "Apr 8, 2010"
 
 DEFAULT_CONTROL_ADDR = "127.0.0.1"
 DEFAULT_CONTROL_PORT = 9051
+DEFAULT_CONFIG = os.path.expanduser("~/.armrc")
 DEFAULT_LOGGED_EVENTS = "N3" # tor and arm NOTICE, WARN, and ERR events
+AUTH_CFG = "init.password" # config option for user's controller password
 
-OPT = "i:p:be:vh"
-OPT_EXPANDED = ["interface=", "password=", "blind", "event=", "version", "help"]
+OPT = "i:c:be:vh"
+OPT_EXPANDED = ["interface=", "config=", "blind", "event=", "version", "help"]
 HELP_MSG = """Usage arm [OPTION]
 Terminal status monitor for Tor relays.
 
   -i, --interface [ADDRESS:]PORT  change control interface from %s:%i
-  -p, --password PASSWORD         authenticate using password (skip prompt)
+  -c, --config CONFIG_PATH        loaded configuration options, CONFIG_PATH
+                                    defaults to: %s
   -b, --blind                     disable connection lookups
   -e, --event EVENT_FLAGS         event types in message log  (default: %s)
 %s
@@ -39,8 +44,8 @@
 
 Example:
 arm -b -i 1643          hide connection data, attaching to control port 1643
-arm -e we -p nemesis    use password 'nemesis' with 'WARN'/'ERR' events
-""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_LOGGED_EVENTS, logPanel.EVENT_LISTING)
+arm -e we -c /tmp/cfg   use this configuration file with 'WARN'/'ERR' events
+""" % (DEFAULT_CONTROL_ADDR, DEFAULT_CONTROL_PORT, DEFAULT_CONFIG, DEFAULT_LOGGED_EVENTS, interface.logPanel.EVENT_LISTING)
 
 def isValidIpAddr(ipStr):
   """
@@ -68,7 +73,7 @@
 if __name__ == '__main__':
   controlAddr = DEFAULT_CONTROL_ADDR     # controller interface IP address
   controlPort = DEFAULT_CONTROL_PORT     # controller interface port
-  authPassword = ""                      # authentication password (prompts if unset and needed)
+  configPath = DEFAULT_CONFIG            # path used for customized configuration
   isBlindMode = False                    # allows connection lookups to be disabled
   loggedEvents = DEFAULT_LOGGED_EVENTS   # flags for event types in message log
   
@@ -102,7 +107,7 @@
       except AssertionError, exc:
         print exc
         sys.exit()
-    elif opt in ("-p", "--password"): authPassword = arg    # sets authentication password
+    elif opt in ("-c", "--config"): configPath = arg        # sets path of user's config
     elif opt in ("-b", "--blind"): isBlindMode = True       # prevents connection lookups
     elif opt in ("-e", "--event"): loggedEvents = arg       # set event flags
     elif opt in ("-v", "--version"):
@@ -112,85 +117,30 @@
       print HELP_MSG
       sys.exit()
   
+  # attempts to load user's custom configuration
+  config = util.conf.getConfig("arm")
+  config.path = configPath
+  
+  try: config.load()
+  except IOError, exc: print "Failed to load configuration (using defaults): %s" % exc
+  
   # validates and expands log event flags
   try:
-    expandedEvents = logPanel.expandEvents(loggedEvents)
+    expandedEvents = interface.logPanel.expandEvents(loggedEvents)
   except ValueError, exc:
     for flag in str(exc):
       print "Unrecognized event flag: %s" % flag
     sys.exit()
   
   # temporarily disables TorCtl logging to prevent issues from going to stdout while starting
-  TorUtil.loglevel = "NONE"
+  TorCtl.TorUtil.loglevel = "NONE"
   
-  # attempts to open a socket to the tor server
-  try:
-    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-    s.connect((controlAddr, controlPort))
-    conn = TorCtl.Connection(s)
-  except socket.error, exc:
-    if str(exc) == "[Errno 111] Connection refused":
-      # most common case - tor control port isn't available
-      print "Connection refused. Is the ControlPort enabled?"
-    else:
-      # less common issue - provide exc message
-      print "Failed to establish socket: %s" % exc
-    
-    sys.exit()
+  # sets up TorCtl connection, prompting for the passphrase if necessary and
+  # printing a notice if they arise
+  authPassword = config.get(AUTH_CFG, None)
+  conn = util.torTools.getConn(controlAddr, controlPort, authPassword)
+  if conn == None: sys.exit(1)
   
-  # check PROTOCOLINFO for authentication type
-  try:
-    authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
-  except TorCtl.ErrorReply, exc:
-    print "Unable to query PROTOCOLINFO for authentication type: %s" % exc
-    sys.exit()
-  
-  try:
-    if authInfo.startswith("AUTH METHODS=NULL"):
-      # no authentication required
-      conn.authenticate("")
-    elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
-      # password authentication, promts for password if it wasn't provided
-      try:
-        if not authPassword: authPassword = getpass.getpass()
-      except KeyboardInterrupt:
-        sys.exit()
-      
-      conn.authenticate(authPassword)
-    elif authInfo.startswith("AUTH METHODS=COOKIE"):
-      # cookie authtication, parses path to authentication cookie
-      start = authInfo.find("COOKIEFILE=\"") + 12
-      end = authInfo[start:].find("\"")
-      authCookiePath = authInfo[start:start + end]
-      
-      try:
-        authCookie = open(authCookiePath, "r")
-        conn.authenticate_cookie(authCookie)
-        authCookie.close()
-      except IOError, exc:
-        # cleaner message for common errors
-        issue = None
-        if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
-        elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
-        
-        # if problem's recognized give concise message, otherwise print exception string
-        if issue: print "Failed to read authentication cookie (%s): %s" % (issue, authCookiePath)
-        else: print "Failed to read authentication cookie: %s" % exc
-        
-        sys.exit()
-    else:
-      # authentication type unrecognized (probably a new addition to the controlSpec)
-      print "Unrecognized authentication type: %s" % authInfo
-      sys.exit()
-  except TorCtl.ErrorReply, exc:
-    # authentication failed
-    issue = str(exc)
-    if str(exc).startswith("515 Authentication failed: Password did not match"): issue = "password incorrect"
-    if str(exc) == "515 Authentication failed: Wrong length on authentication cookie.": issue = "cookie value incorrect"
-    
-    print "Unable to authenticate: %s" % issue
-    sys.exit()
-  
-  controller.startTorMonitor(conn, expandedEvents, isBlindMode)
+  interface.controller.startTorMonitor(conn, expandedEvents, isBlindMode)
   conn.close()
 

Modified: arm/trunk/interface/connPanel.py
===================================================================
--- arm/trunk/interface/connPanel.py	2010-04-25 19:03:47 UTC (rev 22233)
+++ arm/trunk/interface/connPanel.py	2010-04-25 21:15:26 UTC (rev 22234)
@@ -109,7 +109,7 @@
   Lists tor related connection data.
   """
   
-  def __init__(self, stdscr, conn):
+  def __init__(self, stdscr, conn, isDisabled):
     TorCtl.PostEventListener.__init__(self)
     panel.Panel.__init__(self, stdscr, 0)
     self.scroll = 0
@@ -129,7 +129,7 @@
     self.orconnStatusCacheValid = False   # indicates if cache has been invalidated
     self.clientConnectionCache = None     # listing of nicknames for our client connections
     self.clientConnectionLock = RLock()   # lock for clientConnectionCache
-    self.isDisabled = False               # prevent panel from updating entirely
+    self.isDisabled = isDisabled          # prevent panel from updating entirely
     self.lastConnResults = None           # used to check if connection results have changed
     
     self.isCursorEnabled = True
@@ -267,6 +267,8 @@
     Reloads connection results.
     """
     
+    if self.isDisabled: return
+    
     # inaccessable during startup so might need to be refetched
     try:
       if not self.address: self.address = self.conn.get_info("address")["address"]

Modified: arm/trunk/interface/controller.py
===================================================================
--- arm/trunk/interface/controller.py	2010-04-25 19:03:47 UTC (rev 22233)
+++ arm/trunk/interface/controller.py	2010-04-25 21:15:26 UTC (rev 22234)
@@ -387,13 +387,10 @@
   # before being positioned - the following is a quick hack til rewritten
   panels["log"].setPaused(True)
   
-  panels["conn"] = connPanel.ConnPanel(stdscr, conn)
+  panels["conn"] = connPanel.ConnPanel(stdscr, conn, isBlindMode)
   panels["control"] = ControlPanel(stdscr, isBlindMode)
   panels["torrc"] = confPanel.ConfPanel(stdscr, confLocation, conn)
   
-  # prevents connection resolution via the connPanel if not being used
-  if isBlindMode: panels["conn"].isDisabled = True
-  
   # provides error if pid coulnd't be determined (hopefully shouldn't happen...)
   if not torPid: log.log(log.WARN, "Unable to resolve tor pid, abandoning connection listing")
   
@@ -545,7 +542,15 @@
         # quits arm
         # very occasionally stderr gets "close failed: [Errno 11] Resource temporarily unavailable"
         # this appears to be a python bug: http://bugs.python.org/issue3014
-        hostnames.stop()
+        # (haven't seen this is quite some time... mysteriously resolved?)
+        
+        # joins on utility daemon threads - this might take a moment since
+        # the internal threadpools being joined might be sleeping
+        resolver = connections.getResolver("tor") if connections.isResolverAlive("tor") else None
+        if resolver: resolver.stop()  # sets halt flag (returning immediately)
+        hostnames.stop()              # halts and joins on hostname worker thread pool
+        if resolver: resolver.join()  # joins on halted resolver
+        
         conn.close() # joins on TorCtl event thread
         break
     elif key == curses.KEY_LEFT or key == curses.KEY_RIGHT:
@@ -947,10 +952,15 @@
               lookupErrored = False
               if selection in relayLookupCache.keys(): nsEntry, descEntry = relayLookupCache[selection]
               else:
-                # ns lookup fails, can happen with localhost lookups if relay's having problems (orport not reachable)
-                # and this will be empty if network consensus couldn't be fetched
-                try: nsCall = conn.get_network_status("id/%s" % fingerprint)
-                except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed): lookupErrored = True
+                try:
+                  nsCall = conn.get_network_status("id/%s" % fingerprint)
+                  if len(nsCall) == 0: raise TorCtl.ErrorReply() # no results provided
+                except (socket.error, TorCtl.ErrorReply, TorCtl.TorCtlClosed):
+                  # ns lookup fails or provides empty results - can happen with
+                  # localhost lookups if relay's having problems (orport not
+                  # reachable) and this will be empty if network consensus
+                  # couldn't be fetched
+                  lookupErrored = True
                 
                 if not lookupErrored and nsCall:
                   if len(nsCall) > 1:

Added: arm/trunk/util/conf.py
===================================================================
--- arm/trunk/util/conf.py	                        (rev 0)
+++ arm/trunk/util/conf.py	2010-04-25 21:15:26 UTC (rev 22234)
@@ -0,0 +1,150 @@
+"""
+This provides handlers for specially formatted configuration files. Entries are
+expected to consist of simple key/value pairs, and anything after "#" is
+stripped as a comment. Excess whitespace is trimmed and empty lines are
+ignored. For instance:
+# This is my sample config
+
+user.name Galen
+user.password yabba1234 # here's an inline comment
+user.notes takes a fancy to pepperjack chese
+blankEntry.example
+
+would be loaded as four entries (the last one's value being an empty string).
+If a key's defined multiple times then the last instance of it is used.
+"""
+
+import os
+import threading
+
+CONFS = {}  # mapping of identifier to singleton instances of configs
+
+def getConfig(handle):
+  """
+  Singleton constructor for configuration file instances. If a configuration
+  already exists for the handle then it's returned. Otherwise a fresh instance
+  is constructed.
+  
+  Arguments:
+    handle - unique identifier used to access this config instance
+  """
+  
+  if not handle in CONFS.keys(): CONFS[handle] = Config()
+  return CONFS[handle]
+
+class Config():
+  """
+  Handler for easily working with custom configurations, providing persistance
+  to and from files. All operations are thread safe.
+  
+  Parameters:
+    path        - location from which configurations are saved and loaded
+    contents    - mapping of current key/value pairs
+    rawContents - last read/written config (initialized to an empty string)
+  """
+  
+  def __init__(self):
+    """
+    Creates a new configuration instance.
+    """
+    
+    self.path = None        # path to the associated configuation file
+    self.contents = {}      # configuration key/value pairs
+    self.contentsLock = threading.RLock()
+    self.rawContents = []   # raw contents read from configuration file
+  
+  def get(self, key, default=None):
+    """
+    This provides the currently value associated with a given key. If no such
+    key exists then this provides the default.
+    
+    Arguments:
+      key     - config setting to be fetched
+      default - value provided if no such key exists
+    """
+    
+    self.contentsLock.acquire()
+    if key in self.contents.keys(): val = self.contents[key]
+    else: val = default
+    self.contentsLock.release()
+    
+    return val
+  
+  def set(self, key, value):
+    """
+    Stores the given configuration value.
+    
+    Arguments:
+      key   - config key to be set
+      value - config value to be set
+    """
+    
+    self.contentsLock.acquire()
+    self.contents[key] = value
+    self.contentsLock.release()
+  
+  def clear(self):
+    """
+    Drops all current key/value mappings.
+    """
+    
+    self.contentsLock.acquire()
+    self.contents.clear()
+    self.contentsLock.release()
+  
+  def load(self):
+    """
+    Reads in the contents of the currently set configuration file (appending
+    any results to the current configuration). If the file's empty or doesn't
+    exist then this doesn't do anything.
+    
+    Other issues (like having an unset path or insufficient permissions) result
+    in an IOError.
+    """
+    
+    if not self.path: raise IOError("unable to load (config path undefined)")
+    
+    if os.path.exists(self.path):
+      configFile = open(self.path, "r")
+      self.rawContents = configFile.readlines()
+      configFile.close()
+      
+      self.contentsLock.acquire()
+      
+      for line in self.rawContents:
+        # strips any commenting or excess whitespace
+        commentStart = line.find("#")
+        if commentStart != -1: line = line[:commentStart]
+        line = line.strip()
+        
+        # parse the key/value pair
+        if line:
+          if " " in line:
+            key, value = line.split(" ", 1)
+            self.contents[key] = value
+          else:
+            self.contents[line] = "" # no value was provided
+      
+      self.contentsLock.release()
+  
+  def save(self, saveBackup=True):
+    """
+    Writes the contents of the current configuration. If a configuration file
+    already exists then merges as follows:
+    - comments and file contents not in this config are left unchanged
+    - lines with duplicate keys are stripped (first instance is kept)
+    - existing enries are overwritten with their new values, preserving the
+      positioning of inline comments if able
+    - config entries not in the file are appended to the end in alphabetical
+      order
+    
+    If problems arise in writting (such as an unset path or insufficient
+    permissions) result in an IOError.
+    
+    Arguments:
+      saveBackup - if true and a file already exists then it's saved (with
+                   '.backup' appended to its filename)
+    """
+    
+    pass # TODO: implement when persistence is needed
+

Modified: arm/trunk/util/connections.py
===================================================================
--- arm/trunk/util/connections.py	2010-04-25 19:03:47 UTC (rev 22233)
+++ arm/trunk/util/connections.py	2010-04-25 21:15:26 UTC (rev 22234)
@@ -31,11 +31,11 @@
 #   equivilant -p doesn't exist so this can't function)
 RUN_NETSTAT = "netstat -npt 2> /dev/null | grep %s/%s 2> /dev/null"
 
-# p = include process
+# n = numeric ports, p = include process
 # output:
 # ESTAB  0  0  127.0.0.1:9051  127.0.0.1:53308  users:(("tor",9912,20))
 # *note: under freebsd this command belongs to a spreadsheet program
-RUN_SS = "ss -p 2> /dev/null | grep \"\\\"%s\\\",%s\" 2> /dev/null"
+RUN_SS = "ss -np 2> /dev/null | grep \"\\\"%s\\\",%s\" 2> /dev/null"
 
 # n = prevent dns lookups, P = show port numbers (not names), i = ip only
 # output:
@@ -49,6 +49,9 @@
 RESOLVER_SERIAL_FAILURE_MSG = "Querying connections with %s failed, trying %s"
 RESOLVER_FINAL_FAILURE_MSG = "All connection resolvers failed"
 
+# mapping of commands to if they're available or not
+CMD_AVAILABLE_CACHE = {}
+
 def getConnections(resolutionCmd, processName, processPid = ""):
   """
   Retrieves a list of the current connections for a given process, providing a
@@ -66,6 +69,10 @@
     processPid    - process ID (this helps improve accuracy)
   """
   
+  # first check if the command's available to avoid sending error to stdout
+  if not _isAvailable(CMD_STR[resolutionCmd]):
+    raise IOError("'%s' is unavailable" % CMD_STR[resolutionCmd])
+  
   if resolutionCmd == CMD_NETSTAT: cmd = RUN_NETSTAT % (processPid, processName)
   elif resolutionCmd == CMD_SS: cmd = RUN_SS % (processName, processPid)
   else: cmd = RUN_LSOF % (processName, processPid)
@@ -93,8 +100,25 @@
   
   return conn
 
-def getResolver(processName, processPid = "", newInit = True):
+def isResolverAlive(processName, processPid = ""):
   """
+  This provides true if a singleton resolver instance exists for the given
+  process/pid combination, false otherwise.
+  
+  Arguments:
+    processName - name of the process being checked
+    processPid  - pid of the process being checked, if undefined this matches
+                  against any resolver with the process name
+  """
+  
+  for resolver in RESOLVERS:
+    if resolver.processName == processName and (not processPid or resolver.processPid == processPid):
+      return True
+  
+  return False
+
+def getResolver(processName, processPid = ""):
+  """
   Singleton constructor for resolver instances. If a resolver already exists
   for the process then it's returned. Otherwise one is created and started.
   
@@ -102,8 +126,6 @@
     processName - name of the process being resolved
     processPid  - pid of the process being resolved, if undefined this matches
                   against any resolver with the process name
-    newInit     - if a resolver isn't available then one's created if true,
-                  otherwise this returns None
   """
   
   # check if one's already been created
@@ -112,28 +134,36 @@
       return resolver
   
   # make a new resolver
-  if newInit:
-    r = ConnectionResolver(processName, processPid)
-    r.start()
-    RESOLVERS.append(r)
-    return r
-  else: return None
+  r = ConnectionResolver(processName, processPid)
+  r.start()
+  RESOLVERS.append(r)
+  return r
 
-def _isAvailable(command):
+def _isAvailable(command, cached=True):
   """
   Checks the current PATH to see if a command is available or not. This returns
   True if an accessible executable by the name is found and False otherwise.
   
   Arguments:
     command - name of the command for which to search
+    cached  - this makes use of available cached results if true, otherwise
+              they're overwritten
   """
   
-  for path in os.environ["PATH"].split(os.pathsep):
-    cmdPath = os.path.join(path, command)
-    if os.path.exists(cmdPath) and os.access(cmdPath, os.X_OK): return True
-  
-  return False
-  
+  if cached and command in CMD_AVAILABLE_CACHE.keys():
+    return CMD_AVAILABLE_CACHE[command]
+  else:
+    cmdExists = False
+    for path in os.environ["PATH"].split(os.pathsep):
+      cmdPath = os.path.join(path, command)
+      
+      if os.path.exists(cmdPath) and os.access(cmdPath, os.X_OK):
+        cmdExists = True
+        break
+    
+    CMD_AVAILABLE_CACHE[command] = cmdExists
+    return cmdExists
+
 if __name__ == '__main__':
   # quick method for testing connection resolution
   userInput = raw_input("Enter query (RESOLVER PROCESS_NAME [PID]: ").split()

Added: arm/trunk/util/torTools.py
===================================================================
--- arm/trunk/util/torTools.py	                        (rev 0)
+++ arm/trunk/util/torTools.py	2010-04-25 21:15:26 UTC (rev 22234)
@@ -0,0 +1,144 @@
+"""
+Helper for working with an active tor process. This both provides a wrapper for
+accessing TorCtl and notifications of state changes to subscribers.
+"""
+
+import socket
+import getpass
+
+from TorCtl import TorCtl
+
+def makeCtlConn(controlAddr="127.0.0.1", controlPort=9051):
+  """
+  Opens a socket to the tor controller and queries its authentication type,
+  raising an IOError if problems occure. The result of this function is a tuple
+  of the TorCtl connection and the authentication type, where the later is one
+  of the following:
+  "NONE"          - no authentication required
+  "PASSWORD"      - requires authentication via a hashed password
+  "COOKIE=<FILE>" - requires the specified authentication cookie
+  
+  Arguments:
+    controlAddr - ip address belonging to the controller
+    controlPort - port belonging to the controller
+  """
+  
+  try:
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((controlAddr, controlPort))
+    conn = TorCtl.Connection(s)
+  except socket.error, exc:
+    if "Connection refused" in exc.args:
+      # most common case - tor control port isn't available
+      raise IOError("Connection refused. Is the ControlPort enabled?")
+    else: raise IOError("Failed to establish socket: %s" % exc)
+  
+  # check PROTOCOLINFO for authentication type
+  try:
+    authInfo = conn.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
+  except TorCtl.ErrorReply, exc:
+    raise IOError("Unable to query PROTOCOLINFO for authentication type: %s" % exc)
+  
+  if authInfo.startswith("AUTH METHODS=NULL"):
+    # no authentication required
+    return (conn, "NONE")
+  elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
+    # password authentication
+    return (conn, "PASSWORD")
+  elif authInfo.startswith("AUTH METHODS=COOKIE"):
+    # cookie authtication, parses authentication cookie path
+    start = authInfo.find("COOKIEFILE=\"") + 12
+    end = authInfo.find("\"", start)
+    return (conn, "COOKIE=%s" % authInfo[start:end])
+
+def initCtlConn(conn, authType="NONE", authVal=None):
+  """
+  Authenticates to a tor connection. The authentication type can be any of the
+  following strings:
+  NONE, PASSWORD, COOKIE
+  
+  if the authentication type is anything other than NONE then either a
+  passphrase or path to an authentication cookie is expected. If an issue
+  arises this raises either of the following:
+    - IOError for failures in reading an authentication cookie
+    - TorCtl.ErrorReply for authentication failures
+  
+  Argument:
+    conn     - unauthenticated TorCtl connection
+    authType - type of authentication method to use
+    authVal  - passphrase or path to authentication cookie
+  """
+  
+  # validates input
+  if authType not in ("NONE", "PASSWORD", "COOKIE"):
+    # authentication type unrecognized (possibly a new addition to the controlSpec?)
+    raise TorCtl.ErrorReply("Unrecognized authentication type: %s" % authType)
+  elif authType != "NONE" and authVal == None:
+    typeLabel = "passphrase" if authType == "PASSWORD" else "cookie"
+    raise TorCtl.ErrorReply("Unable to authenticate: no %s provided" % typeLabel)
+  
+  authCookie = None
+  try:
+    if authType == "NONE": conn.authenticate("")
+    elif authType == "PASSWORD": conn.authenticate(authVal)
+    else:
+      authCookie = open(authVal, "r")
+      conn.authenticate_cookie(authCookie)
+      authCookie.close()
+  except TorCtl.ErrorReply, exc:
+    if authCookie: authCookie.close()
+    issue = str(exc)
+    
+    # simplifies message if the wrong credentials were provided (common mistake)
+    if issue.startswith("515 Authentication failed: "):
+      if issue[27:].startswith("Password did not match"):
+        issue = "password incorrect"
+      elif issue[27:] == "Wrong length on authentication cookie.":
+        issue = "cookie value incorrect"
+    
+    raise TorCtl.ErrorReply("Unable to authenticate: %s" % issue)
+  except IOError, exc:
+    if authCookie: authCookie.close()
+    issue = None
+    
+    # cleaner message for common errors
+    if str(exc).startswith("[Errno 13] Permission denied"): issue = "permission denied"
+    elif str(exc).startswith("[Errno 2] No such file or directory"): issue = "file doesn't exist"
+    
+    # if problem's recognized give concise message, otherwise print exception string
+    if issue: raise IOError("Failed to read authentication cookie (%s): %s" % (issue, authCookiePath))
+    else: raise IOError("Failed to read authentication cookie: %s" % exc)
+
+def getConn(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
+  """
+  Convenience method for quickly getting a TorCtl connection. This is very
+  handy for debugging or CLI setup, handling setup and prompting for a password
+  if necessary. If any issues arise this prints a description of the problem
+  and returns None.
+  
+  Arguments:
+    controlAddr - ip address belonging to the controller
+    controlPort - port belonging to the controller
+    passphrase  - authentication passphrase (if defined this is used rather
+                  than prompting the user)
+  """
+  
+  try:
+    conn, authType = makeCtlConn(controlAddr, controlPort)
+    authValue = None
+    
+    if authType == "PASSWORD":
+      # password authentication, promting for the password if it wasn't provided
+      if passphrase: authValue = passphrase
+      else:
+        try: authValue = getpass.getpass()
+        except KeyboardInterrupt: return None
+    elif authType.startswith("COOKIE"):
+      authType, authValue = authType.split("=", 1)
+    
+    initCtlConn(conn, authType, authValue)
+    return conn
+  except Exception, exc:
+    print exc
+    return None
+