[or-cvs] [pytorctl/master] Convenience functions for creating and authenticating TorCtl connections.

mikeperry at torproject.org mikeperry at torproject.org
Thu Aug 26 09:20:17 UTC 2010


Author: Damian Johnson <atagar at torproject.org>
Date: Wed, 25 Aug 2010 09:38:04 -0700
Subject: Convenience functions for creating and authenticating TorCtl connections.
Commit: 6a2885fa4f517c74ec0a79ee22a5333dc3a87b0f

---
 TorCtl.py |  182 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 179 insertions(+), 3 deletions(-)

diff --git a/TorCtl.py b/TorCtl.py
index 8aa498b..c5937e9 100755
--- a/TorCtl.py
+++ b/TorCtl.py
@@ -18,12 +18,19 @@ custom event handlers that you may extend off of those).
 
 This package also contains a helper class for representing Routers, and
 classes and constants for each event.
+ 
+To quickly fetch a TorCtl instance to experiment with use the following:
+
+>>> import TorCtl
+>>> conn = TorCtl.connect()
+>>> conn.get_info("version")["version"]
+'0.2.1.24'
 
 """
 
-__all__ = ["EVENT_TYPE", "TorCtlError", "TorCtlClosed", "ProtocolError",
-           "ErrorReply", "NetworkStatus", "ExitPolicyLine", "Router",
-           "RouterVersion", "Connection", "parse_ns_body",
+__all__ = ["EVENT_TYPE", "connect", "TorCtlError", "TorCtlClosed",
+           "ProtocolError", "ErrorReply", "NetworkStatus", "ExitPolicyLine",
+           "Router", "RouterVersion", "Connection", "parse_ns_body",
            "EventHandler", "DebugEventHandler", "NetworkStatusEvent",
            "NewDescEvent", "CircuitEvent", "StreamEvent", "ORConnEvent",
            "StreamBwEvent", "LogEvent", "AddrMapEvent", "BWEvent",
@@ -39,6 +46,7 @@ import Queue
 import datetime
 import traceback
 import socket
+import getpass
 import binascii
 import types
 import time
@@ -79,6 +87,62 @@ EVENT_STATE = Enum2(
           POSTLISTEN="POSTLISTEN",
           DONE="DONE")
 
+# Types of control port authentication
+AUTH_TYPE = Enum2(
+          NONE="NONE",
+          PASSWORD="PASSWORD",
+          COOKIE="COOKIE")
+
+INCORRECT_PASSWORD_MSG = "Provided passphrase was incorrect"
+
+def connect(controlAddr="127.0.0.1", controlPort=9051, passphrase=None):
+  """
+  Convenience function 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 either none is provided as input or it fails). 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:
+    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    s.connect((controlAddr, controlPort))
+    conn = Connection(s)
+    authType, authValue = conn.get_auth_type(), ""
+    
+    if authType == AUTH_TYPE.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 == AUTH_TYPE.COOKIE:
+      authValue = conn.get_auth_cookie_path()
+    
+    conn.authenticate(authValue)
+    return conn
+  except socket.error, exc:
+    if "Connection refused" in exc.args:
+      # most common case - tor control port isn't available
+      print "Connection refused. Is the ControlPort enabled?"
+    else: print "Failed to establish socket: %s" % exc
+    
+    return None
+  except Exception, exc:
+    if passphrase and str(exc) == "Unable to authenticate: password incorrect":
+      # provide a warning that the provided password didn't work, then try
+      # again prompting for the user to enter it
+      print INCORRECT_PASSWORD_MSG
+      return connect(controlAddr, controlPort)
+    else:
+      print exc
+      return None
+
 class TorCtlError(Exception):
   "Generic error raised by TorControl code."
   pass
@@ -487,7 +551,64 @@ class Connection:
     self._eventQueue = Queue.Queue()
     self._s = BufSock(sock)
     self._debugFile = None
+    
+    # authentication information (lazily fetched so None if still unknown)
+    self._authType = None
+    self._cookiePath = None
 
+  def get_auth_type(self):
+    """
+    Provides the authentication type used for the control port (a member of
+    the AUTH_TYPE enumeration). This raises an IOError if this fails to query
+    the PROTOCOLINFO.
+    """
+    
+    if self._authType: return self._authType
+    else:
+      # check PROTOCOLINFO for authentication type
+      try:
+        authInfo = self.sendAndRecv("PROTOCOLINFO\r\n")[1][1]
+      except ErrorReply, exc:
+        raise IOError("Unable to query PROTOCOLINFO for the authentication type: %s" % exc)
+      
+      authType, cookiePath = None, None
+      if authInfo.startswith("AUTH METHODS=NULL"):
+        # no authentication required
+        authType = AUTH_TYPE.NONE
+      elif authInfo.startswith("AUTH METHODS=HASHEDPASSWORD"):
+        # password authentication
+        authType = AUTH_TYPE.PASSWORD
+      elif authInfo.startswith("AUTH METHODS=COOKIE"):
+        # cookie authentication, parses authentication cookie path
+        authType = AUTH_TYPE.COOKIE
+        
+        start = authInfo.find("COOKIEFILE=\"") + 12
+        end = authInfo.find("\"", start)
+        cookiePath = authInfo[start:end]
+      else:
+        # not of a recognized authentication type (new addition to the
+        # control-spec?)
+        raise IOError("Unrecognized authentication type: %s" % authInfo)
+      
+      self._authType = authType
+      self._cookiePath = cookiePath
+      return self._authType
+  
+  def get_auth_cookie_path(self):
+    """
+    Provides the path of tor's authentication cookie. If the connection isn't
+    using cookie authentication then this provides None. This raises an IOError
+    if PROTOCOLINFO can't be queried.
+    """
+    
+    # fetches authentication type and cookie path if still unloaded
+    if self._authType == None: self.get_auth_type()
+    
+    if self._authType == AUTH_TYPE.COOKIE:
+      return self._cookiePath
+    else:
+      return None
+  
   def set_close_handler(self, handler):
     """Call 'handler' when the Tor process has closed its connection or
        given us an exception.  If we close normally, no arguments are
@@ -765,6 +886,61 @@ class Connection:
     return lines
 
   def authenticate(self, secret=""):
+    """
+    Authenticates to the control port. If an issue arises this raises either of
+    the following:
+      - IOError for failures in reading an authentication cookie or querying
+        PROTOCOLINFO.
+      - TorCtl.ErrorReply for authentication failures or if the secret is
+        undefined when using password authentication
+    """
+    
+    # fetches authentication type and cookie path if still unloaded
+    if self._authType == None: self.get_auth_type()
+    
+    # validates input
+    if self._authType == AUTH_TYPE.PASSWORD and secret == "":
+      raise ErrorReply("Unable to authenticate: no passphrase provided")
+    
+    authCookie = None
+    try:
+      if self._authType == AUTH_TYPE.NONE:
+        self.authenticate_password("")
+      elif self._authType == AUTH_TYPE.PASSWORD:
+        self.authenticate_password(secret)
+      else:
+        authCookie = open(self._cookiePath, "r")
+        self.authenticate_cookie(authCookie)
+        authCookie.close()
+    except 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 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, self._cookiePath))
+      else: raise IOError("Failed to read authentication cookie: %s" % exc)
+  
+  def authenticate_password(self, secret=""):
     """Sends an authenticating secret (password) to Tor.  You'll need to call 
        this method (or authenticate_cookie) before Tor can start.
     """
-- 
1.7.1



More information about the tor-commits mailing list