[tor-commits] [stem/master] Add GetConfResponse class for parsing GETCONF responses

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


commit 5701329478dfc363da17d80cafca26f07a9a9def
Author: Ravi Chandra Padmala <neenaoffline at gmail.com>
Date:   Fri Jun 8 12:41:09 2012 +0530

    Add GetConfResponse class for parsing GETCONF responses
---
 run_tests.py                  |    2 +
 stem/response/__init__.py     |   12 +++++-
 stem/response/getconf.py      |   53 +++++++++++++++++++++++++
 test/unit/response/getconf.py |   87 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 153 insertions(+), 1 deletions(-)

diff --git a/run_tests.py b/run_tests.py
index 7ccedd3..ea12531 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -22,6 +22,7 @@ import test.unit.descriptor.extrainfo_descriptor
 import test.unit.response.control_line
 import test.unit.response.control_message
 import test.unit.response.getinfo
+import test.unit.response.getconf
 import test.unit.response.protocolinfo
 import test.unit.response.authchallenge
 import test.unit.util.conf
@@ -110,6 +111,7 @@ UNIT_TESTS = (
   test.unit.response.control_message.TestControlMessage,
   test.unit.response.control_line.TestControlLine,
   test.unit.response.getinfo.TestGetInfoResponse,
+  test.unit.response.getconf.TestGetConfResponse,
   test.unit.response.protocolinfo.TestProtocolInfoResponse,
   test.unit.response.authchallenge.TestAuthChallengeResponse,
   test.unit.connection.authentication.TestAuthenticate,
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index dfae1d3..ae3af4c 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -25,7 +25,7 @@ Parses replies from the control socket.
 
 from __future__ import with_statement
 
-__all__ = ["getinfo", "protocolinfo", "authchallenge", "convert", "ControlMessage", "ControlLine"]
+__all__ = ["getinfo", "getconf", "protocolinfo", "authchallenge", "convert", "ControlMessage", "ControlLine"]
 
 import re
 import threading
@@ -49,6 +49,7 @@ def convert(response_type, message):
   subclass for its response type. Recognized types include...
   
     * GETINFO
+    * GETCONF
     * PROTOCOLINFO
     * AUTHCHALLENGE
   
@@ -59,10 +60,12 @@ def convert(response_type, message):
   
   :raises:
     * :class:`stem.socket.ProtocolError` the message isn't a proper response of that type
+    * :class:`stem.response.InvalidRequest` the request was invalid
     * TypeError if argument isn't a :class:`stem.response.ControlMessage` or response_type isn't supported
   """
   
   import stem.response.getinfo
+  import stem.response.getconf
   import stem.response.protocolinfo
   import stem.response.authchallenge
   
@@ -71,6 +74,8 @@ def convert(response_type, message):
   
   if response_type == "GETINFO":
     response_class = stem.response.getinfo.GetInfoResponse
+  elif response_type == "GETCONF":
+    response_class = stem.response.getconf.GetConfResponse
   elif response_type == "PROTOCOLINFO":
     response_class = stem.response.protocolinfo.ProtocolInfoResponse
   elif response_type == "AUTHCHALLENGE":
@@ -408,3 +413,8 @@ def _get_quote_indeces(line, escaped):
   
   return tuple(indices)
 
+class InvalidRequest(Exception):
+  """
+  Base Exception class for invalid requests
+  """
+  pass
diff --git a/stem/response/getconf.py b/stem/response/getconf.py
new file mode 100644
index 0000000..8dcd483
--- /dev/null
+++ b/stem/response/getconf.py
@@ -0,0 +1,53 @@
+import re
+
+import stem.socket
+import stem.response
+
+class GetConfResponse(stem.response.ControlMessage):
+  """
+  Reply for a GETCONF query.
+  
+  :var dict entries: mapping between the queried options and their values
+  """
+  
+  def _parse_message(self):
+    # Example:
+    # 250-CookieAuthentication=0
+    # 250-ControlPort=9100
+    # 250-DataDirectory=/home/neena/.tor
+    # 250 DirPort
+    
+    self.entries = {}
+    remaining_lines = list(self)
+
+    if self.content() == [("250", " ", "OK")]: return
+    
+    if not self.is_ok():
+      unrecognized_keywords = []
+      for code, _, line in self.content():
+        if code == '552':
+          try:
+            # to parse: 552 Unrecognized configuration key "zinc"
+            unrecognized_keywords.append(re.search('"([^"]+)"', line).groups()[0])
+          except:
+            pass
+
+      if unrecognized_keywords:
+        raise stem.response.InvalidRequest("GETCONF request contained unrecognized keywords: %s\n" \
+            % ', '.join(unrecognized_keywords))
+      else:
+        raise stem.socket.ProtocolError("GETCONF response contained a non-OK status code:\n%s" % self)
+    
+    while remaining_lines:
+      line = remaining_lines.pop(0)
+
+      if '=' in line:
+        if line[line.find("=") + 1] == "\"":
+          key, value = line.pop_mapping(True)
+        else:
+          key, value = line.split("=", 1)
+      else:
+        key, value = (line, None)
+      
+      self.entries[key] = value
+
diff --git a/test/unit/response/getconf.py b/test/unit/response/getconf.py
new file mode 100644
index 0000000..b82256b
--- /dev/null
+++ b/test/unit/response/getconf.py
@@ -0,0 +1,87 @@
+"""
+Unit tests for the stem.response.getconf.GetConfResponse class.
+"""
+
+import unittest
+
+import stem.socket
+import stem.response
+import stem.response.getinfo
+import test.mocking as mocking
+
+EMPTY_RESPONSE = "250 OK"
+
+SINGLE_RESPONSE = """\
+250 DataDirectory=/home/neena/.tor"""
+
+BATCH_RESPONSE = """\
+250-CookieAuthentication=0
+250-ControlPort=9100
+250-DataDirectory=/tmp/fake dir
+250 DirPort"""
+
+UNRECOGNIZED_KEY_RESPONSE = "552 Unrecognized configuration key \"yellowbrickroad\""
+
+INVALID_RESPONSE = """\
+123-FOO
+232 BAR"""
+
+class TestGetConfResponse(unittest.TestCase):
+  def test_empty_response(self):
+    """
+    Parses a GETCONF reply without options (just calling "GETCONF").
+    """
+    
+    control_message = mocking.get_message(EMPTY_RESPONSE)
+    stem.response.convert("GETCONF", control_message)
+    
+    # now this should be a GetConfResponse (ControlMessage subclass)
+    self.assertTrue(isinstance(control_message, stem.response.ControlMessage))
+    self.assertTrue(isinstance(control_message, stem.response.getconf.GetConfResponse))
+    
+    self.assertEqual({}, control_message.entries)
+  
+  def test_single_response(self):
+    """
+    Parses a GETCONF reply response for a single parameter.
+    """
+    
+    control_message = mocking.get_message(SINGLE_RESPONSE)
+    stem.response.convert("GETCONF", control_message)
+    self.assertEqual({"DataDirectory": "/home/neena/.tor"}, control_message.entries)
+  
+  def test_batch_response(self):
+    """
+    Parses a GETCONF reply for muiltiple parameters.
+    """
+    
+    control_message = mocking.get_message(BATCH_RESPONSE)
+    stem.response.convert("GETCONF", control_message)
+    
+    expected = {
+      "CookieAuthentication": "0",
+      "ControlPort": "9100",
+      "DataDirectory": "/tmp/fake dir",
+      "DirPort": None,
+    }
+    
+    self.assertEqual(expected, control_message.entries)
+  
+  def test_unrecognized_key_response(self):
+    """
+    Parses a GETCONF reply that contains an error code with an unrecognized key.
+    """
+    
+    control_message = mocking.get_message(UNRECOGNIZED_KEY_RESPONSE)
+    self.assertRaises(stem.response.InvalidRequest, stem.response.convert, "GETCONF", control_message)
+  
+  def test_invalid_multiline_content(self):
+    """
+    Parses a malformed GETCONF reply that contains an invalid response code.
+    This is a proper controller message, but malformed according to the
+    GETCONF's spec.
+    """
+    
+    control_message = mocking.get_message(INVALID_RESPONSE)
+    self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETCONF", control_message)
+





More information about the tor-commits mailing list