[tor-commits] [stem/master] Parsing and class for PROTOCOLINFO responses

atagar at torproject.org atagar at torproject.org
Sun Nov 13 19:05:14 UTC 2011


commit 5756f9940ff0bbd55330a700d648af347dec713a
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Nov 13 02:19:26 2011 -0800

    Parsing and class for PROTOCOLINFO responses
    
    Finally have enough plumbing in place to write the parsing for the PROTOCOLINFO
    queries. I'm pretty happy with how it turned out - next is testing for the
    class, then moving on to functions for issuing the PROTOCOLINFO queries.
---
 stem/connection.py |  177 +++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 175 insertions(+), 2 deletions(-)

diff --git a/stem/connection.py b/stem/connection.py
index 4076400..d17efde 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -4,11 +4,184 @@ Functions for connecting and authenticating to the tor process.
 
 import Queue
 import socket
+import logging
 import threading
 
 import stem.types
+import stem.util.enum
+import stem.util.system
 
-from stem.util import log
+LOGGER = logging.getLogger("stem")
+
+# Methods by which a controller can authenticate to the control port. Tor gives
+# a list of all the authentication methods it will accept in response to
+# PROTOCOLINFO queries.
+#
+# NONE     - No authentication required
+# PASSWORD - See tor's HashedControlPassword option. Controllers must provide
+#            the password used to generate the hash.
+# COOKIE   - See tor's CookieAuthentication option. Controllers need to supply
+#            the contents of the cookie file.
+# UNKNOWN  - Tor provided one or more authentication methods that we don't
+#            recognize. This is probably from a new addition to the control
+#            protocol.
+
+AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "UNKNOWN")
+
+class ProtocolInfoResponse(stem.types.ControlMessage):
+  """
+  Version one PROTOCOLINFO query response.
+  
+  According to the control spec the cookie_file is an absolute path. However,
+  this often is not the case (especially for the Tor Browser Bundle)...
+  https://trac.torproject.org/projects/tor/ticket/1101
+  
+  If the path is relative then we'll make an attempt (which may not work) to
+  correct this.
+  
+  The protocol_version is the only mandatory data for a valid PROTOCOLINFO
+  response, so all other values are None if undefined or empty if a collecion.
+  
+  Attributes:
+    protocol_version (int)           - protocol version of the response
+    tor_version (stem.types.Version) - version of the tor process
+    auth_methods (tuple)             - AuthMethod types that tor will accept
+    unknown_auth_methods (tuple)     - strings of unrecognized auth methods
+    cookie_file (str)                - path of tor's authentication cookie
+    socket (socket.socket)           - socket used to make the query
+  """
+  
+  def convert(control_message):
+    """
+    Parses a ControlMessage, converting it into a ProtocolInfoResponse.
+    
+    Arguments:
+      control_message (stem.types.ControlMessage) -
+        message to be parsed as a PROTOCOLINFO reply
+    
+    Raises:
+      stem.types.ProtocolError the message isn't a proper PROTOCOLINFO response
+      ValueError if argument is of the wrong type
+    """
+    
+    if isinstance(control_message, stem.types.ControlMessage):
+      control_message.__class__ = ProtocolInfoResponse
+      control_message._parse_message()
+      return control_message
+    else:
+      raise ValueError("Only able to convert stem.types.ControlMessage instances")
+  
+  convert = staticmethod(convert)
+  
+  def _parse_message(self):
+    # Example:
+    #   250-PROTOCOLINFO 1
+    #   250-AUTH METHODS=COOKIE COOKIEFILE="/home/atagar/.tor/control_auth_cookie"
+    #   250-VERSION Tor="0.2.1.30"
+    #   250 OK
+    
+    self.protocol_version = None
+    self.tor_version = None
+    self.cookie_file = None
+    self.socket = None
+    
+    auth_methods, unknown_auth_methods = [], []
+    
+    # sanity check that we're a PROTOCOLINFO response
+    if not list(self)[0].startswith("PROTOCOLINFO"):
+      msg = "Message is not a PROTOCOLINFO response"
+      raise stem.types.ProtocolError(msg)
+    
+    for line in self:
+      if line == "OK": break
+      elif line.is_empty(): continue # blank line
+      
+      line_type = line.pop()
+      
+      if line_type == "PROTOCOLINFO":
+        # Line format:
+        #   FirstLine = "PROTOCOLINFO" SP PIVERSION CRLF
+        #   PIVERSION = 1*DIGIT
+        
+        if line.is_empty():
+          msg = "PROTOCOLINFO response's initial line is missing the protocol version: %s" % line
+          raise stem.types.ProtocolError(msg)
+        
+        piversion = line.pop()
+        
+        if not piversion.isdigit():
+          msg = "PROTOCOLINFO response version is non-numeric: %s" % line
+          raise stem.types.ProtocolError(msg)
+        
+        self.protocol_version = int(piversion)
+        
+        # The piversion really should be "1" but, according to the spec, tor
+        # does not necessarily need to provide the PROTOCOLINFO version that we
+        # requested. Log if it's something we aren't expecting but still make
+        # an effort to parse like a v1 response.
+        
+        if self.protocol_version != 1:
+          LOGGER.warn("We made a PROTOCOLINFO v1 query but got a version %i response instead. We'll still try to use it, but this may cause problems." % self.protocol_version)
+      elif line_type == "AUTH":
+        # Line format:
+        #   AuthLine = "250-AUTH" SP "METHODS=" AuthMethod *("," AuthMethod)
+        #              *(SP "COOKIEFILE=" AuthCookieFile) CRLF
+        #   AuthMethod = "NULL" / "HASHEDPASSWORD" / "COOKIE"
+        #   AuthCookieFile = QuotedString
+        
+        # parse AuthMethod mapping
+        if not line.is_next_mapping("METHODS"):
+          msg = "PROTOCOLINFO response's AUTH line is missing its mandatory 'METHODS' mapping: %s" % line
+          raise stem.types.ProtocolError(msg)
+        
+        for method in line.pop_mapping()[1].split(","):
+          if method == "NULL":
+            auth_methods.append(AuthMethod.NONE)
+          elif method == "HASHEDPASSWORD":
+            auth_methods.append(AuthMethod.PASSWORD)
+          elif method == "COOKIE":
+            auth_methods.append(AuthMethod.COOKIE)
+          else:
+            unknown_auth_methods.append(method)
+            LOGGER.info("PROTOCOLINFO response had an unrecognized authentication method: %s" % method)
+            
+            # our auth_methods should have a single AuthMethod.UNKNOWN entry if
+            # any unknown authentication methods exist
+            if not AuthMethod.UNKNOWN in auth_methods:
+              auth_methods.append(AuthMethod.UNKNOWN)
+        
+        # parse optional COOKIEFILE mapping (quoted and can have escapes)
+        if line.is_next_mapping("COOKIEFILE", True, True):
+          self.cookie_file = line.pop_mapping(True, True)[0]
+          
+          # attempt to expand relative cookie paths
+          if stem.util.system.is_relative_path(self.cookie_file):
+            try:
+              tor_pid = stem.util.system.get_pid("tor", suppress_exc = False)
+              tor_cwd = stem.util.system.get_cwd(tor_pid, False)
+              self.cookie_file = stem.util.system.expand_path(self.cookie_file, tor_cwd)
+            except IOError, exc:
+              LOGGER.debug("unable to expand relative tor cookie path: %s" % exc)
+      elif line_type == "VERSION":
+        # Line format:
+        #   VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
+        #   TorVersion = QuotedString
+        
+        if not line.is_next_mapping("Tor", True):
+          msg = "PROTOCOLINFO response's VERSION line is missing its mandatory tor version mapping: %s" % line
+          raise stem.types.ProtocolError(msg)
+        
+        torversion = line.pop_mapping(True)[1]
+        
+        try:
+          self.tor_version = stem.types.Version(torversion)
+        except ValueError, exc:
+          raise stem.types.ProtocolError(exc)
+      else:
+        LOGGER.debug("unrecognized PROTOCOLINFO line type '%s', ignoring entry: %s" % (line_type, line))
+    
+    self.auth_methods = tuple(auth_methods)
+    self.unknown_auth_methods = tuple(unknown_auth_methods)
 
 class ControlConnection:
   """
@@ -126,7 +299,7 @@ class ControlConnection:
           # TODO: figure out a good method for terminating the socket thread
           self._reply_queue.put(control_message)
       except stem.types.ProtocolError, exc:
-        log.log(log.ERR, "Error reading control socket message: %s" % exc)
+        LOGGER.error("Error reading control socket message: %s" % exc)
         # TODO: terminate?
   
   def close(self):



More information about the tor-commits mailing list