[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Parsing and class for PROTOCOLINFO responses
commit 5756f9940ff0bbd55330a700d648af347dec713a
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
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):
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits