[tor-commits] [stem/master] Various modifications to GETCONF parsing

atagar at torproject.org atagar at torproject.org
Wed Jul 4 21:34:20 UTC 2012


commit b8959dfe154180018071a07d7ad8eceb68a4c516
Author: Ravi Chandra Padmala <neenaoffline at gmail.com>
Date:   Sun Jun 17 18:05:54 2012 +0530

    Various modifications to GETCONF parsing
    
    * Change the way entries are stored. Now only stores one value unless the caller
      explicitly mentions that he want to retrieve multiple values using the
      'multiple' argument.
    
    * Stop checking if the received configuration options are the ones that were
      requested. (The HiddenService options do this)
    
    * Minor documentation fixes.
---
 stem/control.py                  |   35 +++++++++++++++------------------
 stem/response/__init__.py        |    5 ++-
 stem/response/getconf.py         |   40 +++++++++++++++++++-------------------
 stem/response/getinfo.py         |    2 +-
 stem/socket.py                   |   16 +++++++-------
 test/integ/control/controller.py |   12 +++++-----
 test/unit/response/getconf.py    |   15 ++++++-------
 7 files changed, 61 insertions(+), 64 deletions(-)

diff --git a/stem/control.py b/stem/control.py
index a616b5c..d14d723 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -453,7 +453,9 @@ class Controller(BaseController):
       * dict with the param => response mapping if our param was a list
       * default if one was provided and our call failed
     
-    :raises: :class:`stem.socket.ControllerError` if the call fails, and we weren't provided a default response
+    :raises:
+      :class:`stem.socket.ControllerError` if the call fails, and we weren't provided a default response
+      :class:`stem.socket.InvalidArguments` if the 'param' requested was invalid
     """
     
     # TODO: add caching?
@@ -530,8 +532,8 @@ class Controller(BaseController):
     """
     
     return stem.connection.get_protocolinfo(self)
-
-  def get_conf(self, param, default = None):
+  
+  def get_conf(self, param, default = UNDEFINED, multiple = False):
     """
     Queries the control socket for the values of given configuration options. If
     provided a default then that's returned as if the GETCONF option is undefined
@@ -540,17 +542,21 @@ class Controller(BaseController):
     
     :param str,list param: GETCONF option or options to be queried
     :param object default: response if the query fails
+    :param bool multiple: if True, the value(s) provided are lists of all returned values,
+                          otherwise this just provides the first value
     
     :returns:
       Response depends upon how we were called as follows...
       
-      * str with the response if our param was a str
-      * dict with the param => response mapping if our param was a list
+      * str with the response if our param was a str and multiple was False
+      * dict with the param (str) => response (str) mapping if our param was a list and multiple was False
+      * list with the response strings if our param was a str and multiple was True
+      * dict with the param (str) => response (list) mapping if our param was a list and multiple was True
       * default if one was provided and our call failed
     
     :raises:
       :class:`stem.socket.ControllerError` if the call fails, and we weren't provided a default response
-      :class:`stem.socket.InvalidArguments` if configuration options requested was invalid
+      :class:`stem.socket.InvalidArguments` if the configuration options requested were invalid
     """
     
     if isinstance(param, str):
@@ -561,23 +567,14 @@ class Controller(BaseController):
     
     try:
       response = self.msg("GETCONF %s" % " ".join(param))
-      stem.response.convert("GETCONF", response)
-      
-      # error if we got back different parameters than we requested
-      requested_params = set(param)
-      reply_params = set(response.entries.keys())
-      
-      if requested_params != reply_params:
-        requested_label = ", ".join(requested_params)
-        reply_label = ", ".join(reply_params)
-        
-        raise stem.socket.ProtocolError("GETCONF reply doesn't match the parameters that we requested. Queried '%s' but got '%s'." % (requested_label, reply_label))
+      stem.response.convert("GETCONF", response, multiple = multiple)
       
       if is_multiple:
         return response.entries
       else:
-        return response.entries[param[0]]
+        try: return response.entries[param[0]]
+        except KeyError: raise stem.socket.InvalidRequest("Received empty string")
     except stem.socket.ControllerError, exc:
-      if default is None: raise exc
+      if default is UNDEFINED: raise exc
       else: return default
 
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index 13c65b3..590e4c2 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -42,7 +42,7 @@ KEY_ARG = re.compile("^(\S+)=")
 CONTROL_ESCAPES = {r"\\": "\\",  r"\"": "\"",   r"\'": "'",
                    r"\r": "\r",  r"\n": "\n",   r"\t": "\t"}
 
-def convert(response_type, message):
+def convert(response_type, message, **kwargs):
   """
   Converts a ControlMessage into a particular kind of tor response. This does
   an in-place conversion of the message from being a ControlMessage to a
@@ -57,6 +57,7 @@ def convert(response_type, message):
   
   :param str response_type: type of tor response to convert to
   :param stem.response.ControlMessage message: message to be converted
+  :param kwargs: optional keyword arguments to be passed to the parser method
   
   :raises:
     * :class:`stem.socket.ProtocolError` the message isn't a proper response of that type
@@ -84,7 +85,7 @@ def convert(response_type, message):
   else: raise TypeError("Unsupported response type: %s" % response_type)
   
   message.__class__ = response_class
-  message._parse_message()
+  message._parse_message(**kwargs)
 
 class ControlMessage:
   """
diff --git a/stem/response/getconf.py b/stem/response/getconf.py
index fd2256c..9ab15c7 100644
--- a/stem/response/getconf.py
+++ b/stem/response/getconf.py
@@ -1,14 +1,6 @@
 import stem.socket
 import stem.response
 
-def _split_line(line):
-  if line.is_next_mapping(quoted = False):
-    return line.split("=", 1) # TODO: make this part of the ControlLine?
-  elif line.is_next_mapping(quoted = True):
-    return line.pop_mapping(True).items()[0]
-  else:
-    return (line.pop(), None)
-
 class GetConfResponse(stem.response.ControlMessage):
   """
   Reply for a GETCONF query.
@@ -18,7 +10,12 @@ class GetConfResponse(stem.response.ControlMessage):
     of strings)
   """
   
-  def _parse_message(self):
+  def _parse_message(self, multiple = False):
+    """
+    :param bool multiple:
+      if True stores each value in a list, otherwise stores only the first
+      values as a sting
+    """
     # Example:
     # 250-CookieAuthentication=0
     # 250-ControlPort=9100
@@ -27,7 +24,7 @@ class GetConfResponse(stem.response.ControlMessage):
     
     self.entries = {}
     remaining_lines = list(self)
-
+    
     if self.content() == [("250", " ", "OK")]: return
     
     if not self.is_ok():
@@ -35,7 +32,7 @@ class GetConfResponse(stem.response.ControlMessage):
       for code, _, line in self.content():
         if code == "552" and line.startswith("Unrecognized configuration key \"") and line.endswith("\""):
           unrecognized_keywords.append(line[32:-1])
-
+      
       if unrecognized_keywords:
         raise stem.socket.InvalidArguments("552", "GETCONF request contained unrecognized keywords: %s\n" \
             % ', '.join(unrecognized_keywords), unrecognized_keywords)
@@ -44,15 +41,18 @@ class GetConfResponse(stem.response.ControlMessage):
     
     while remaining_lines:
       line = remaining_lines.pop(0)
-
-      key, value = _split_line(line)
+      
+      if line.is_next_mapping(quoted = False):
+        key, value = line.split("=", 1) # TODO: make this part of the ControlLine?
+      elif line.is_next_mapping(quoted = True):
+        key, value = line.pop_mapping(True).items()[0]
+      else:
+        key, value = (line.pop(), None)
+      
       entry = self.entries.get(key, None)
-
-      if type(entry) == str and entry != value:
-        self.entries[key] = [entry]
-        self.entries[key].append(value)
-      elif type(entry) == list and not value in entry:
-        self.entries[key].append(value)
+      
+      if multiple:
+        self.entries.setdefault(key, []).append(value)
       else:
-        self.entries[key] = value
+        self.entries.setdefault(key, value)
 
diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py
index f467769..a2cce57 100644
--- a/stem/response/getinfo.py
+++ b/stem/response/getinfo.py
@@ -29,7 +29,7 @@ class GetInfoResponse(stem.response.ControlMessage):
       for code, _, line in self.content():
         if code == '552' and line.startswith("Unrecognized key \"") and line.endswith("\""):
           unrecognized_keywords.append(line[18:-1])
-
+      
       if unrecognized_keywords:
         raise stem.socket.InvalidArguments("552", "GETINFO request contained unrecognized keywords: %s\n" \
             % ', '.join(unrecognized_keywords), unrecognized_keywords)
diff --git a/stem/socket.py b/stem/socket.py
index 9e1f5b7..6972929 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -29,7 +29,7 @@ as instances of the :class:`stem.response.ControlMessage` class.
   ControllerError - Base exception raised when using the controller.
     |- ProtocolError - Malformed socket data.
     |- InvalidRequest - Invalid request.
-       +- InvalidArguments - Invalid request parameters.
+    |  +- InvalidArguments - Invalid request parameters.
     +- SocketError - Communication with the socket failed.
        +- SocketClosed - Socket has been shut down.
 """
@@ -553,7 +553,7 @@ class ProtocolError(ControllerError):
 class InvalidRequest(ControllerError):
   """
   Base Exception class for invalid requests
-
+  
   :var str code: The error code returned by Tor (if applicable)
   :var str message: The error message returned by Tor (if applicable) or a human
     readable error message
@@ -562,11 +562,11 @@ class InvalidRequest(ControllerError):
   def __init__(self, code = None, message = None):
     """
     Initializes an InvalidRequest object.
-
+    
     :param str code: The error code returned by Tor (if applicable)
     :param str message: The error message returned by Tor (if applicable) or a
       human readable error message
-
+    
     :returns: object of InvalidRequest class
     """
     
@@ -576,22 +576,22 @@ class InvalidRequest(ControllerError):
 class InvalidArguments(InvalidRequest):
   """
   Exception class for invalid requests which contain invalid arguments.
-
+  
   :var str code: The error code returned by Tor (if applicable)
   :var str message: The error message returned by Tor (if applicable) or a human
     readable error message
   :var list arguments: a list of arguments which were invalid
   """
-
+  
   def __init__(self, code = None, message = None, arguments = None):
     """
     Initializes an InvalidArguments object.
-
+    
     :param str code: The error code returned by Tor (if applicable)
     :param str message: The error message returned by Tor (if applicable) or a
       human readable error message
     :param list arguments: a list of arguments which were invalid
-
+    
     :returns: object of InvalidArguments class
     """
     
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index 2441eac..db7941c 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -147,18 +147,18 @@ class TestController(unittest.TestCase):
       
       socket = runner.get_tor_socket()
       if isinstance(socket, stem.socket.ControlPort):
-        socket = str(socket.get_port())
+        connection_value = str(socket.get_port())
         config_key = "ControlPort"
       elif isinstance(socket, stem.socket.ControlSocketFile):
-        socket = str(socket.get_socket_path())
+        connection_value = str(socket.get_socket_path())
         config_key = "ControlSocket"
       
-      self.assertEqual(socket, controller.get_conf(config_key))
-      self.assertEqual(socket, controller.get_conf(config_key, "la-di-dah"))
+      self.assertEqual(connection_value, controller.get_conf(config_key))
+      self.assertEqual(connection_value, controller.get_conf(config_key, "la-di-dah"))
       
       # succeessful batch query
       
-      expected = {config_key: socket}
+      expected = {config_key: connection_value}
       self.assertEqual(expected, controller.get_conf([config_key]))
       self.assertEqual(expected, controller.get_conf([config_key], "la-di-dah"))
       
@@ -173,7 +173,7 @@ class TestController(unittest.TestCase):
       
       # empty input
       
-      self.assertRaises(stem.socket.ControllerError, controller.get_conf, "")
+      self.assertRaises(stem.socket.InvalidRequest, controller.get_conf, "")
       self.assertEqual("la-di-dah", controller.get_conf("", "la-di-dah"))
       
       self.assertEqual({}, controller.get_conf([]))
diff --git a/test/unit/response/getconf.py b/test/unit/response/getconf.py
index a9f1cc8..e0020fb 100644
--- a/test/unit/response/getconf.py
+++ b/test/unit/response/getconf.py
@@ -24,12 +24,11 @@ MULTIVALUE_RESPONSE = """\
 250-ControlPort=9100
 250-ExitPolicy=accept 34.3.4.5
 250-ExitPolicy=accept 3.4.53.3
-250-ExitPolicy=reject 23.245.54.3
-250-ExitPolicy=accept 34.3.4.5
 250-ExitPolicy=accept 3.4.53.3
 250 ExitPolicy=reject 23.245.54.3"""
 
-UNRECOGNIZED_KEY_RESPONSE = "552 Unrecognized configuration key \"yellowbrickroad\""
+UNRECOGNIZED_KEY_RESPONSE = '''552-Unrecognized configuration key "brickroad"
+552 Unrecognized configuration key "submarine"'''
 
 INVALID_RESPONSE = """\
 123-FOO
@@ -82,11 +81,11 @@ class TestGetConfResponse(unittest.TestCase):
     """
     
     control_message = mocking.get_message(MULTIVALUE_RESPONSE)
-    stem.response.convert("GETCONF", control_message)
+    stem.response.convert("GETCONF", control_message, multiple = True)
     
     expected = {
-      "ControlPort": "9100",
-      "ExitPolicy": ["accept 34.3.4.5", "accept 3.4.53.3", "reject 23.245.54.3"]
+      "ControlPort": ["9100"],
+      "ExitPolicy": ["accept 34.3.4.5", "accept 3.4.53.3", "accept 3.4.53.3", "reject 23.245.54.3"]
     }
     
     self.assertEqual(expected, control_message.entries)
@@ -100,9 +99,9 @@ class TestGetConfResponse(unittest.TestCase):
     self.assertRaises(stem.socket.InvalidArguments, stem.response.convert, "GETCONF", control_message)
     
     try:
-      stem.response.convert("GETCONF", control_message)
+      stem.response.convert("GETCONF", control_message, multiple = True)
     except stem.socket.InvalidArguments, exc:
-      self.assertEqual(exc.arguments, ["yellowbrickroad"])
+      self.assertEqual(exc.arguments, ["brickroad", "submarine"])
   
   def test_invalid_content(self):
     """





More information about the tor-commits mailing list