commit 5701329478dfc363da17d80cafca26f07a9a9def Author: Ravi Chandra Padmala neenaoffline@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) +
tor-commits@lists.torproject.org