[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
+