commit 1d7ab654ec473a5e54ea3253f82abdcadc93158f Author: Damian Johnson atagar@torproject.org Date: Mon May 28 15:12:16 2012 -0700
Moving tor response classes into their own module
As we add more response classes it'll be messy to sprinkle them all about the codebase. Making a single 'stem.response' module that'll contain them all. These could probably do with some more love so I'll next see if I can make them any tidier. --- run_tests.py | 12 +- stem/__init__.py | 2 +- stem/connection.py | 164 +----------------------------- stem/control.py | 67 +------------ stem/response/__init__.py | 46 +++++++++ stem/response/getinfo.py | 43 ++++++++ stem/response/protocolinfo.py | 131 ++++++++++++++++++++++++ test/integ/connection/__init__.py | 2 +- test/integ/connection/protocolinfo.py | 138 ------------------------- test/integ/response/__init__.py | 6 + test/integ/response/protocolinfo.py | 138 +++++++++++++++++++++++++ test/mocking.py | 8 +- test/unit/connection/__init__.py | 2 +- test/unit/connection/protocolinfo.py | 178 -------------------------------- test/unit/control/getinfo.py | 119 ---------------------- test/unit/response/__init__.py | 6 + test/unit/response/getinfo.py | 120 ++++++++++++++++++++++ test/unit/response/protocolinfo.py | 180 +++++++++++++++++++++++++++++++++ 18 files changed, 689 insertions(+), 673 deletions(-)
diff --git a/run_tests.py b/run_tests.py index 13fb027..dea86c3 100755 --- a/run_tests.py +++ b/run_tests.py @@ -16,13 +16,13 @@ import test.output import test.runner import test.check_whitespace import test.unit.connection.authentication -import test.unit.connection.protocolinfo -import test.unit.control.getinfo import test.unit.socket.control_line import test.unit.socket.control_message import test.unit.descriptor.reader import test.unit.descriptor.server_descriptor import test.unit.descriptor.extrainfo_descriptor +import test.unit.response.getinfo +import test.unit.response.protocolinfo import test.unit.util.conf import test.unit.util.connection import test.unit.util.enum @@ -31,7 +31,6 @@ import test.unit.util.tor_tools import test.unit.version import test.integ.connection.authentication import test.integ.connection.connect -import test.integ.connection.protocolinfo import test.integ.control.base_controller import test.integ.control.controller import test.integ.socket.control_message @@ -39,6 +38,7 @@ import test.integ.socket.control_socket import test.integ.descriptor.reader import test.integ.descriptor.server_descriptor import test.integ.descriptor.extrainfo_descriptor +import test.integ.response.protocolinfo import test.integ.util.conf import test.integ.util.system import test.integ.process @@ -100,11 +100,11 @@ UNIT_TESTS = ( test.unit.descriptor.server_descriptor.TestServerDescriptor, test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor, test.unit.version.TestVersion, + test.unit.response.getinfo.TestGetInfoResponse, + test.unit.response.protocolinfo.TestProtocolInfoResponse, test.unit.socket.control_message.TestControlMessage, test.unit.socket.control_line.TestControlLine, test.unit.connection.authentication.TestAuthenticate, - test.unit.connection.protocolinfo.TestProtocolInfoResponse, - test.unit.control.getinfo.TestGetInfoResponse, )
INTEG_TESTS = ( @@ -114,10 +114,10 @@ INTEG_TESTS = ( test.integ.descriptor.server_descriptor.TestServerDescriptor, test.integ.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor, test.integ.version.TestVersion, + test.integ.response.protocolinfo.TestProtocolInfo, test.integ.process.TestProcess, test.integ.socket.control_socket.TestControlSocket, test.integ.socket.control_message.TestControlMessage, - test.integ.connection.protocolinfo.TestProtocolInfo, test.integ.connection.authentication.TestAuthenticate, test.integ.connection.connect.TestConnect, test.integ.control.base_controller.TestBaseController, diff --git a/stem/__init__.py b/stem/__init__.py index ceb137f..30e4f81 100644 --- a/stem/__init__.py +++ b/stem/__init__.py @@ -2,5 +2,5 @@ Library for working with the tor process. """
-__all__ = ["descriptor", "util", "connection", "control", "process", "socket", "version"] +__all__ = ["descriptor", "response", "util", "connection", "control", "process", "socket", "version"]
diff --git a/stem/connection.py b/stem/connection.py index 983eb84..34c9c68 100644 --- a/stem/connection.py +++ b/stem/connection.py @@ -48,14 +48,6 @@ authenticate_password - Authenticates to a socket supporting password auth. authenticate_cookie - Authenticates to a socket supporting cookie auth.
get_protocolinfo - Issues a PROTOCOLINFO query. -ProtocolInfoResponse - Reply from a PROTOCOLINFO query. - |- Attributes: - | |- protocol_version - | |- tor_version - | |- auth_methods - | |- unknown_auth_methods - | +- cookie_path - +- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse
AuthenticationFailure - Base exception raised for authentication failures. |- UnrecognizedAuthMethods - Authentication methods are unsupported. @@ -84,6 +76,7 @@ import os import getpass import binascii
+import stem.response import stem.socket import stem.version import stem.util.enum @@ -295,7 +288,7 @@ def authenticate(controller, password = None, chroot_path = None, protocolinfo_r password (str) - passphrase to present to the socket if it uses password authentication (skips password auth if None) chroot_path (str) - path prefix if in a chroot environment - protocolinfo_response (stem.connection.ProtocolInfoResponse) - + protocolinfo_response (stem.response.protocolinfo.ProtocolInfoResponse) - tor protocolinfo response, this is retrieved on our own if None
Raises: @@ -640,7 +633,7 @@ def get_protocolinfo(controller): tor controller connection
Returns: - stem.connection.ProtocolInfoResponse provided by tor + stem.response.protocolinfo.ProtocolInfoResponse provided by tor
Raises: stem.socket.ProtocolError if the PROTOCOLINFO response is malformed @@ -664,7 +657,7 @@ def get_protocolinfo(controller): except stem.socket.SocketClosed, exc: raise stem.socket.SocketError(exc)
- ProtocolInfoResponse.convert(protocolinfo_response) + stem.response.convert("PROTOCOLINFO", protocolinfo_response)
# attempt to expand relative cookie paths via the control port or socket file
@@ -723,152 +716,3 @@ def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg)
protocolinfo_response.cookie_path = cookie_path
-class ProtocolInfoResponse(stem.socket.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 collection. - - Attributes: - protocol_version (int) - protocol version of the response - tor_version (stem.version.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_path (str) - path of tor's authentication cookie - """ - - def convert(control_message): - """ - Parses a ControlMessage, performing an in-place conversion of it into a - ProtocolInfoResponse. - - Arguments: - control_message (stem.socket.ControlMessage) - - message to be parsed as a PROTOCOLINFO reply - - Raises: - stem.socket.ProtocolError the message isn't a proper PROTOCOLINFO response - TypeError if argument isn't a ControlMessage - """ - - if isinstance(control_message, stem.socket.ControlMessage): - control_message.__class__ = ProtocolInfoResponse - control_message._parse_message() - return control_message - else: - raise TypeError("Only able to convert stem.socket.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_path = 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 (%s)" % self - raise stem.socket.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.socket.ProtocolError(msg) - - piversion = line.pop() - - if not piversion.isdigit(): - msg = "PROTOCOLINFO response version is non-numeric: %s" % line - raise stem.socket.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: - log.info("We made a PROTOCOLINFO version 1 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.socket.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) - message_id = "stem.connection.unknown_auth_%s" % method - log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %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_path = line.pop_mapping(True, True)[1] - - # attempt to expand relative cookie paths - _expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor") - 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.socket.ProtocolError(msg) - - torversion = line.pop_mapping(True)[1] - - try: - self.tor_version = stem.version.Version(torversion) - except ValueError, exc: - raise stem.socket.ProtocolError(exc) - else: - log.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) - diff --git a/stem/control.py b/stem/control.py index ccb57f4..b188ea4 100644 --- a/stem/control.py +++ b/stem/control.py @@ -26,6 +26,7 @@ import time import Queue import threading
+import stem.response import stem.socket import stem.util.log as log
@@ -475,7 +476,7 @@ class Controller(BaseController): if response.content()[0][0] != "250": raise stem.socket.ControllerError(str(response))
- GetInfoResponse.convert(response) + stem.response.convert("GETINFO", response)
# error if we got back different parameters than we requested requested_params = set(param) @@ -495,67 +496,3 @@ class Controller(BaseController): if default == UNDEFINED: raise exc else: return default
-class GetInfoResponse(stem.socket.ControlMessage): - """ - Reply for a GETINFO query. - - Attributes: - values (dict) - mapping between the queried options and their values - """ - - def convert(control_message): - """ - Parses a ControlMessage, performing an in-place conversion of it into a - GetInfoResponse. - - Arguments: - control_message (stem.socket.ControlMessage) - - message to be parsed as a GETINFO reply - - Raises: - stem.socket.ProtocolError the message isn't a proper GETINFO response - TypeError if argument isn't a ControlMessage - """ - - if isinstance(control_message, stem.socket.ControlMessage): - control_message.__class__ = GetInfoResponse - control_message._parse_message() - return control_message - else: - raise TypeError("Only able to convert stem.socket.ControlMessage instances") - - convert = staticmethod(convert) - - def _parse_message(self): - # Example: - # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c) - # 250+config-text= - # ControlPort 9051 - # DataDirectory /home/atagar/.tor - # ExitPolicy reject *:* - # Log notice stdout - # Nickname Unnamed - # ORPort 9050 - # . - # 250 OK - - self.values = {} - - for line in self: - if line == "OK": break - elif not "=" in line: - raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line) - - key, value = line.split("=", 1) - - # if the value is a multiline value then it *must* be of the form - # '<key>=\n<value>' - - if "\n" in value: - if value.startswith("\n"): - value = value[1:] - else: - raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line) - - self.values[key] = value - diff --git a/stem/response/__init__.py b/stem/response/__init__.py new file mode 100644 index 0000000..92b9846 --- /dev/null +++ b/stem/response/__init__.py @@ -0,0 +1,46 @@ +""" +Parses replies from the control socket. + +converts - translates a ControlMessage into a particular response subclass +""" + +__all__ = ["getinfo", "protocolinfo"] + +import stem.socket + +def convert(response_type, message): + """ + 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 + subclass for its response type. Recognized types include... + + * GETINFO + * PROTOCOLINFO + + If the response_type isn't recognized then this is leaves it alone. + + Arguments: + response_type (str) - type of tor response to convert to + message (stem.socket.ControlMessage) - message to be converted + + Raises: + stem.socket.ProtocolError the message isn't a proper response of that type + TypeError if argument isn't a ControlMessage or response_type isn't + supported + """ + + import stem.response.getinfo + import stem.response.protocolinfo + + if not isinstance(message, stem.socket.ControlMessage): + raise TypeError("Only able to convert stem.socket.ControlMessage instances") + + if response_type == "GETINFO": + response_class = stem.response.getinfo.GetInfoResponse + elif response_type == "PROTOCOLINFO": + response_class = stem.response.protocolinfo.ProtocolInfoResponse + else: raise TypeError("Unsupported response type: %s" % response_type) + + message.__class__ = response_class + message._parse_message() + diff --git a/stem/response/getinfo.py b/stem/response/getinfo.py new file mode 100644 index 0000000..a13a18f --- /dev/null +++ b/stem/response/getinfo.py @@ -0,0 +1,43 @@ +import stem.socket + +class GetInfoResponse(stem.socket.ControlMessage): + """ + Reply for a GETINFO query. + + Attributes: + values (dict) - mapping between the queried options and their values + """ + + def _parse_message(self): + # Example: + # 250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c) + # 250+config-text= + # ControlPort 9051 + # DataDirectory /home/atagar/.tor + # ExitPolicy reject *:* + # Log notice stdout + # Nickname Unnamed + # ORPort 9050 + # . + # 250 OK + + self.values = {} + + for line in self: + if line == "OK": break + elif not "=" in line: + raise stem.socket.ProtocolError("GETINFO replies should only contain parameter=value mappings: %s" % line) + + key, value = line.split("=", 1) + + # if the value is a multiline value then it *must* be of the form + # '<key>=\n<value>' + + if "\n" in value: + if value.startswith("\n"): + value = value[1:] + else: + raise stem.socket.ProtocolError("GETINFO response contained a multiline value that didn't start with a newline: %s" % line) + + self.values[key] = value + diff --git a/stem/response/protocolinfo.py b/stem/response/protocolinfo.py new file mode 100644 index 0000000..4547a1d --- /dev/null +++ b/stem/response/protocolinfo.py @@ -0,0 +1,131 @@ +import stem.connection +import stem.socket +import stem.version +import stem.util.log as log + +class ProtocolInfoResponse(stem.socket.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 collection. + + Attributes: + protocol_version (int) - protocol version of the response + tor_version (stem.version.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_path (str) - path of tor's authentication cookie + """ + + 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_path = 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 (%s)" % self + raise stem.socket.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.socket.ProtocolError(msg) + + piversion = line.pop() + + if not piversion.isdigit(): + msg = "PROTOCOLINFO response version is non-numeric: %s" % line + raise stem.socket.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: + log.info("We made a PROTOCOLINFO version 1 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.socket.ProtocolError(msg) + + for method in line.pop_mapping()[1].split(","): + if method == "NULL": + auth_methods.append(stem.connection.AuthMethod.NONE) + elif method == "HASHEDPASSWORD": + auth_methods.append(stem.connection.AuthMethod.PASSWORD) + elif method == "COOKIE": + auth_methods.append(stem.connection.AuthMethod.COOKIE) + else: + unknown_auth_methods.append(method) + message_id = "stem.connection.unknown_auth_%s" % method + log.log_once(message_id, log.INFO, "PROTOCOLINFO response included a type of authentication that we don't recognize: %s" % method) + + # our auth_methods should have a single AuthMethod.UNKNOWN entry if + # any unknown authentication methods exist + if not stem.connection.AuthMethod.UNKNOWN in auth_methods: + auth_methods.append(stem.connection.AuthMethod.UNKNOWN) + + # parse optional COOKIEFILE mapping (quoted and can have escapes) + if line.is_next_mapping("COOKIEFILE", True, True): + self.cookie_path = line.pop_mapping(True, True)[1] + + # attempt to expand relative cookie paths + stem.connection._expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor") + 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.socket.ProtocolError(msg) + + torversion = line.pop_mapping(True)[1] + + try: + self.tor_version = stem.version.Version(torversion) + except ValueError, exc: + raise stem.socket.ProtocolError(exc) + else: + log.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) + diff --git a/test/integ/connection/__init__.py b/test/integ/connection/__init__.py index 90471c0..eec60e6 100644 --- a/test/integ/connection/__init__.py +++ b/test/integ/connection/__init__.py @@ -2,5 +2,5 @@ Integration tests for stem.connection. """
-__all__ = ["authenticate", "connect", "protocolinfo"] +__all__ = ["authenticate", "connect"]
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py deleted file mode 100644 index 728c746..0000000 --- a/test/integ/connection/protocolinfo.py +++ /dev/null @@ -1,138 +0,0 @@ -""" -Integration tests for the stem.connection.ProtocolInfoResponse class and -related functions. -""" - -import unittest - -import test.runner -import stem.socket -import stem.connection -import stem.util.system -import test.mocking as mocking -from test.integ.util.system import filter_system_call - -class TestProtocolInfo(unittest.TestCase): - def setUp(self): - test.runner.require_control(self) - mocking.mock(stem.util.proc.is_available, mocking.return_false()) - mocking.mock(stem.util.system.is_available, mocking.return_true()) - - def tearDown(self): - mocking.revert_mocking() - - def test_parsing(self): - """ - Makes a PROTOCOLINFO query and processes the response for our control - connection. - """ - - control_socket = test.runner.get_runner().get_tor_socket(False) - control_socket.send("PROTOCOLINFO 1") - protocolinfo_response = control_socket.recv() - stem.connection.ProtocolInfoResponse.convert(protocolinfo_response) - control_socket.close() - - # according to the control spec the following _could_ differ or be - # undefined but if that actually happens then it's gonna make people sad - - self.assertEqual(1, protocolinfo_response.protocol_version) - self.assertNotEqual(None, protocolinfo_response.tor_version) - self.assertNotEqual(None, protocolinfo_response.auth_methods) - - self.assert_matches_test_config(protocolinfo_response) - - def test_get_protocolinfo_path_expansion(self): - """ - If we're running with the 'RELATIVE' target then test_parsing() will - exercise cookie path expansion when we're able to query the pid by our - prcess name. This test selectively disables system.call() so we exercise - the expansion via our control port or socket file. - - This test is largely redundant with test_parsing() if we aren't running - with the 'RELATIVE' target. - """ - - if test.runner.Torrc.PORT in test.runner.get_runner().get_options(): - cwd_by_port_lookup_prefixes = ( - stem.util.system.GET_PID_BY_PORT_NETSTAT, - stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "", - stem.util.system.GET_PID_BY_PORT_LSOF, - stem.util.system.GET_CWD_PWDX % "", - "lsof -a -p ") - - mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes)) - control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT) - else: - cwd_by_socket_lookup_prefixes = ( - stem.util.system.GET_PID_BY_FILE_LSOF % "", - stem.util.system.GET_CWD_PWDX % "", - "lsof -a -p ") - - mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes)) - control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH) - - protocolinfo_response = stem.connection.get_protocolinfo(control_socket) - self.assert_matches_test_config(protocolinfo_response) - - # we should have a usable socket at this point - self.assertTrue(control_socket.is_alive()) - control_socket.close() - - def test_multiple_protocolinfo_calls(self): - """ - Tests making repeated PROTOCOLINFO queries. This use case is interesting - because tor will shut down the socket and stem should transparently - re-establish it. - """ - - with test.runner.get_runner().get_tor_socket(False) as control_socket: - for i in range(5): - protocolinfo_response = stem.connection.get_protocolinfo(control_socket) - self.assert_matches_test_config(protocolinfo_response) - - def test_pre_disconnected_query(self): - """ - Tests making a PROTOCOLINFO query when previous use of the socket had - already disconnected it. - """ - - with test.runner.get_runner().get_tor_socket(False) as control_socket: - # makes a couple protocolinfo queries outside of get_protocolinfo first - control_socket.send("PROTOCOLINFO 1") - control_socket.recv() - - control_socket.send("PROTOCOLINFO 1") - control_socket.recv() - - protocolinfo_response = stem.connection.get_protocolinfo(control_socket) - self.assert_matches_test_config(protocolinfo_response) - - def assert_matches_test_config(self, protocolinfo_response): - """ - Makes assertions that the protocolinfo response's attributes match those of - the test configuration. - """ - - runner = test.runner.get_runner() - tor_options = runner.get_options() - auth_methods, auth_cookie_path = [], None - - if test.runner.Torrc.COOKIE in tor_options: - auth_methods.append(stem.connection.AuthMethod.COOKIE) - chroot_path = runner.get_chroot() - auth_cookie_path = runner.get_auth_cookie_path() - - if chroot_path and auth_cookie_path.startswith(chroot_path): - auth_cookie_path = auth_cookie_path[len(chroot_path):] - - if test.runner.Torrc.PASSWORD in tor_options: - auth_methods.append(stem.connection.AuthMethod.PASSWORD) - - if not auth_methods: - auth_methods.append(stem.connection.AuthMethod.NONE) - - self.assertEqual((), protocolinfo_response.unknown_auth_methods) - self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods) - self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path) - diff --git a/test/integ/response/__init__.py b/test/integ/response/__init__.py new file mode 100644 index 0000000..2e3a991 --- /dev/null +++ b/test/integ/response/__init__.py @@ -0,0 +1,6 @@ +""" +Integration tests for stem.response. +""" + +__all__ = ["protocolinfo"] + diff --git a/test/integ/response/protocolinfo.py b/test/integ/response/protocolinfo.py new file mode 100644 index 0000000..f5eb518 --- /dev/null +++ b/test/integ/response/protocolinfo.py @@ -0,0 +1,138 @@ +""" +Integration tests for the stem.response.protocolinfo.ProtocolInfoResponse class +and related functions. +""" + +import unittest + +import test.runner +import stem.socket +import stem.connection +import stem.util.system +import test.mocking as mocking +from test.integ.util.system import filter_system_call + +class TestProtocolInfo(unittest.TestCase): + def setUp(self): + test.runner.require_control(self) + mocking.mock(stem.util.proc.is_available, mocking.return_false()) + mocking.mock(stem.util.system.is_available, mocking.return_true()) + + def tearDown(self): + mocking.revert_mocking() + + def test_parsing(self): + """ + Makes a PROTOCOLINFO query and processes the response for our control + connection. + """ + + control_socket = test.runner.get_runner().get_tor_socket(False) + control_socket.send("PROTOCOLINFO 1") + protocolinfo_response = control_socket.recv() + stem.response.convert("PROTOCOLINFO", protocolinfo_response) + control_socket.close() + + # according to the control spec the following _could_ differ or be + # undefined but if that actually happens then it's gonna make people sad + + self.assertEqual(1, protocolinfo_response.protocol_version) + self.assertNotEqual(None, protocolinfo_response.tor_version) + self.assertNotEqual(None, protocolinfo_response.auth_methods) + + self.assert_matches_test_config(protocolinfo_response) + + def test_get_protocolinfo_path_expansion(self): + """ + If we're running with the 'RELATIVE' target then test_parsing() will + exercise cookie path expansion when we're able to query the pid by our + prcess name. This test selectively disables system.call() so we exercise + the expansion via our control port or socket file. + + This test is largely redundant with test_parsing() if we aren't running + with the 'RELATIVE' target. + """ + + if test.runner.Torrc.PORT in test.runner.get_runner().get_options(): + cwd_by_port_lookup_prefixes = ( + stem.util.system.GET_PID_BY_PORT_NETSTAT, + stem.util.system.GET_PID_BY_PORT_SOCKSTAT % "", + stem.util.system.GET_PID_BY_PORT_LSOF, + stem.util.system.GET_CWD_PWDX % "", + "lsof -a -p ") + + mocking.mock(stem.util.system.call, filter_system_call(cwd_by_port_lookup_prefixes)) + control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT) + else: + cwd_by_socket_lookup_prefixes = ( + stem.util.system.GET_PID_BY_FILE_LSOF % "", + stem.util.system.GET_CWD_PWDX % "", + "lsof -a -p ") + + mocking.mock(stem.util.system.call, filter_system_call(cwd_by_socket_lookup_prefixes)) + control_socket = stem.socket.ControlSocketFile(test.runner.CONTROL_SOCKET_PATH) + + protocolinfo_response = stem.connection.get_protocolinfo(control_socket) + self.assert_matches_test_config(protocolinfo_response) + + # we should have a usable socket at this point + self.assertTrue(control_socket.is_alive()) + control_socket.close() + + def test_multiple_protocolinfo_calls(self): + """ + Tests making repeated PROTOCOLINFO queries. This use case is interesting + because tor will shut down the socket and stem should transparently + re-establish it. + """ + + with test.runner.get_runner().get_tor_socket(False) as control_socket: + for i in range(5): + protocolinfo_response = stem.connection.get_protocolinfo(control_socket) + self.assert_matches_test_config(protocolinfo_response) + + def test_pre_disconnected_query(self): + """ + Tests making a PROTOCOLINFO query when previous use of the socket had + already disconnected it. + """ + + with test.runner.get_runner().get_tor_socket(False) as control_socket: + # makes a couple protocolinfo queries outside of get_protocolinfo first + control_socket.send("PROTOCOLINFO 1") + control_socket.recv() + + control_socket.send("PROTOCOLINFO 1") + control_socket.recv() + + protocolinfo_response = stem.connection.get_protocolinfo(control_socket) + self.assert_matches_test_config(protocolinfo_response) + + def assert_matches_test_config(self, protocolinfo_response): + """ + Makes assertions that the protocolinfo response's attributes match those of + the test configuration. + """ + + runner = test.runner.get_runner() + tor_options = runner.get_options() + auth_methods, auth_cookie_path = [], None + + if test.runner.Torrc.COOKIE in tor_options: + auth_methods.append(stem.connection.AuthMethod.COOKIE) + chroot_path = runner.get_chroot() + auth_cookie_path = runner.get_auth_cookie_path() + + if chroot_path and auth_cookie_path.startswith(chroot_path): + auth_cookie_path = auth_cookie_path[len(chroot_path):] + + if test.runner.Torrc.PASSWORD in tor_options: + auth_methods.append(stem.connection.AuthMethod.PASSWORD) + + if not auth_methods: + auth_methods.append(stem.connection.AuthMethod.NONE) + + self.assertEqual((), protocolinfo_response.unknown_auth_methods) + self.assertEqual(tuple(auth_methods), protocolinfo_response.auth_methods) + self.assertEqual(auth_cookie_path, protocolinfo_response.cookie_path) + diff --git a/test/mocking.py b/test/mocking.py index aa61c76..f17dcc9 100644 --- a/test/mocking.py +++ b/test/mocking.py @@ -18,7 +18,7 @@ Mocking Functions
Instance Constructors get_message - stem.socket.ControlMessage - get_protocolinfo_response - stem.connection.ProtocolInfoResponse + get_protocolinfo_response - stem.response.protocolinfo.ProtocolInfoResponse """
import inspect @@ -26,7 +26,7 @@ import itertools import StringIO import __builtin__
-import stem.connection +import stem.response import stem.socket
# Once we've mocked a function we can't rely on its __module__ or __name__ @@ -223,11 +223,11 @@ def get_protocolinfo_response(**attributes): attributes (dict) - attributes to customize the response with
Returns: - stem.connection.ProtocolInfoResponse instance + stem.response.protocolinfo.ProtocolInfoResponse instance """
protocolinfo_response = get_message("250-PROTOCOLINFO 1\n250 OK") - stem.connection.ProtocolInfoResponse.convert(protocolinfo_response) + stem.response.convert("PROTOCOLINFO", protocolinfo_response)
for attr in attributes: protocolinfo_response.__dict__[attr] = attributes[attr] diff --git a/test/unit/connection/__init__.py b/test/unit/connection/__init__.py index 4eae0fa..7073319 100644 --- a/test/unit/connection/__init__.py +++ b/test/unit/connection/__init__.py @@ -2,5 +2,5 @@ Unit tests for stem.connection. """
-__all__ = ["authentication", "protocolinfo"] +__all__ = ["authentication"]
diff --git a/test/unit/connection/protocolinfo.py b/test/unit/connection/protocolinfo.py deleted file mode 100644 index 795d780..0000000 --- a/test/unit/connection/protocolinfo.py +++ /dev/null @@ -1,178 +0,0 @@ -""" -Unit tests for the stem.connection.ProtocolInfoResponse class. -""" - -import unittest - -import stem.connection -import stem.socket -import stem.version -import stem.util.proc -import stem.util.system -import test.mocking as mocking - -NO_AUTH = """250-PROTOCOLINFO 1 -250-AUTH METHODS=NULL -250-VERSION Tor="0.2.1.30" -250 OK""" - -PASSWORD_AUTH = """250-PROTOCOLINFO 1 -250-AUTH METHODS=HASHEDPASSWORD -250-VERSION Tor="0.2.1.30" -250 OK""" - -COOKIE_AUTH = r"""250-PROTOCOLINFO 1 -250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\"dir//control_auth_cookie" -250-VERSION Tor="0.2.1.30" -250 OK""" - -MULTIPLE_AUTH = """250-PROTOCOLINFO 1 -250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie" -250-VERSION Tor="0.2.1.30" -250 OK""" - -UNKNOWN_AUTH = """250-PROTOCOLINFO 1 -250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST -250-VERSION Tor="0.2.1.30" -250 OK""" - -MINIMUM_RESPONSE = """250-PROTOCOLINFO 5 -250 OK""" - -RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1 -250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie" -250-VERSION Tor="0.2.1.30" -250 OK""" - -class TestProtocolInfoResponse(unittest.TestCase): - def test_convert(self): - """ - Exercises functionality of the convert method both when it works and - there's an error. - """ - - # working case - control_message = mocking.get_message(NO_AUTH) - stem.connection.ProtocolInfoResponse.convert(control_message) - - # now this should be a ProtocolInfoResponse (ControlMessage subclass) - self.assertTrue(isinstance(control_message, stem.socket.ControlMessage)) - self.assertTrue(isinstance(control_message, stem.connection.ProtocolInfoResponse)) - - # exercise some of the ControlMessage functionality - raw_content = (NO_AUTH + "\n").replace("\n", "\r\n") - self.assertEquals(raw_content, control_message.raw_content()) - self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1")) - - # attempt to convert the wrong type - self.assertRaises(TypeError, stem.connection.ProtocolInfoResponse.convert, "hello world") - - # attempt to convert a different message type - bw_event_control_message = mocking.get_message("650 BW 32326 2856") - self.assertRaises(stem.socket.ProtocolError, stem.connection.ProtocolInfoResponse.convert, bw_event_control_message) - - def test_no_auth(self): - """ - Checks a response when there's no authentication. - """ - - control_message = mocking.get_message(NO_AUTH) - stem.connection.ProtocolInfoResponse.convert(control_message) - - self.assertEquals(1, control_message.protocol_version) - self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version) - self.assertEquals((stem.connection.AuthMethod.NONE, ), control_message.auth_methods) - self.assertEquals((), control_message.unknown_auth_methods) - self.assertEquals(None, control_message.cookie_path) - - def test_password_auth(self): - """ - Checks a response with password authentication. - """ - - control_message = mocking.get_message(PASSWORD_AUTH) - stem.connection.ProtocolInfoResponse.convert(control_message) - self.assertEquals((stem.connection.AuthMethod.PASSWORD, ), control_message.auth_methods) - - def test_cookie_auth(self): - """ - Checks a response with cookie authentication and a path including escape - characters. - """ - - control_message = mocking.get_message(COOKIE_AUTH) - stem.connection.ProtocolInfoResponse.convert(control_message) - self.assertEquals((stem.connection.AuthMethod.COOKIE, ), control_message.auth_methods) - self.assertEquals("/tmp/my data\"dir//control_auth_cookie", control_message.cookie_path) - - def test_multiple_auth(self): - """ - Checks a response with multiple authentication methods. - """ - - control_message = mocking.get_message(MULTIPLE_AUTH) - stem.connection.ProtocolInfoResponse.convert(control_message) - self.assertEquals((stem.connection.AuthMethod.COOKIE, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods) - self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path) - - def test_unknown_auth(self): - """ - Checks a response with an unrecognized authtentication method. - """ - - control_message = mocking.get_message(UNKNOWN_AUTH) - stem.connection.ProtocolInfoResponse.convert(control_message) - self.assertEquals((stem.connection.AuthMethod.UNKNOWN, stem.connection.AuthMethod.PASSWORD), control_message.auth_methods) - self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods) - - def test_minimum_response(self): - """ - Checks a PROTOCOLINFO response that only contains the minimum amount of - information to be a valid response. - """ - - control_message = mocking.get_message(MINIMUM_RESPONSE) - stem.connection.ProtocolInfoResponse.convert(control_message) - - self.assertEquals(5, control_message.protocol_version) - self.assertEquals(None , control_message.tor_version) - self.assertEquals((), control_message.auth_methods) - self.assertEquals((), control_message.unknown_auth_methods) - self.assertEquals(None, control_message.cookie_path) - - def test_relative_cookie(self): - """ - Checks an authentication cookie with a relative path where expansion both - succeeds and fails. - """ - - # we need to mock both pid and cwd lookups since the general cookie - # expanion works by... - # - resolving the pid of the "tor" process - # - using that to get tor's cwd - - def call_mocking(command): - if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor": - return ["10"] - elif command == stem.util.system.GET_CWD_PWDX % 10: - return ["10: /tmp/foo"] - - mocking.mock(stem.util.proc.is_available, mocking.return_false()) - mocking.mock(stem.util.system.is_available, mocking.return_true()) - mocking.mock(stem.util.system.call, call_mocking) - - control_message = mocking.get_message(RELATIVE_COOKIE_PATH) - stem.connection.ProtocolInfoResponse.convert(control_message) - self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path) - - # exercise cookie expansion where both calls fail (should work, just - # leaving the path unexpanded) - - mocking.mock(stem.util.system.call, mocking.return_none()) - control_message = mocking.get_message(RELATIVE_COOKIE_PATH) - stem.connection.ProtocolInfoResponse.convert(control_message) - self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path) - - # reset system call mocking - mocking.revert_mocking() - diff --git a/test/unit/control/getinfo.py b/test/unit/control/getinfo.py deleted file mode 100644 index 3fc9fd3..0000000 --- a/test/unit/control/getinfo.py +++ /dev/null @@ -1,119 +0,0 @@ -""" -Unit tests for the stem.control.GetInfoResponse class. -""" - -import unittest - -import stem.connection -import test.mocking as mocking - -EMPTY_RESPONSE = "250 OK" - -SINGLE_RESPONSE = """\ -250-version=0.2.3.11-alpha-dev -250 OK""" - -BATCH_RESPONSE = """\ -250-version=0.2.3.11-alpha-dev -250-address=67.137.76.214 -250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0 -250 OK""" - -MULTILINE_RESPONSE = """\ -250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c) -250+config-text= -ControlPort 9051 -DataDirectory /home/atagar/.tor -ExitPolicy reject *:* -Log notice stdout -Nickname Unnamed -ORPort 9050 -. -250 OK""" - -NON_KEY_VALUE_ENTRY = """\ -250-version=0.2.3.11-alpha-dev -250-address 67.137.76.214 -250 OK""" - -MISSING_MULTILINE_NEWLINE = """\ -250+config-text=ControlPort 9051 -DataDirectory /home/atagar/.tor -. -250 OK""" - -class TestGetInfoResponse(unittest.TestCase): - def test_empty_response(self): - """ - Parses a GETINFO reply without options (just calling "GETINFO"). - """ - - control_message = mocking.get_message(EMPTY_RESPONSE) - stem.control.GetInfoResponse.convert(control_message) - - # now this should be a GetInfoResponse (ControlMessage subclass) - self.assertTrue(isinstance(control_message, stem.socket.ControlMessage)) - self.assertTrue(isinstance(control_message, stem.control.GetInfoResponse)) - - self.assertEqual({}, control_message.values) - - def test_single_response(self): - """ - Parses a GETINFO reply response for a single parameter. - """ - - control_message = mocking.get_message(SINGLE_RESPONSE) - stem.control.GetInfoResponse.convert(control_message) - self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values) - - def test_batch_response(self): - """ - Parses a GETINFO reply for muiltiple parameters. - """ - - control_message = mocking.get_message(BATCH_RESPONSE) - stem.control.GetInfoResponse.convert(control_message) - - expected = { - "version": "0.2.3.11-alpha-dev", - "address": "67.137.76.214", - "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0", - } - - self.assertEqual(expected, control_message.values) - - def test_multiline_response(self): - """ - Parses a GETINFO reply for multiple parameters including a multi-line - value. - """ - - control_message = mocking.get_message(MULTILINE_RESPONSE) - stem.control.GetInfoResponse.convert(control_message) - - expected = { - "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)", - "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]), - } - - self.assertEqual(expected, control_message.values) - - def test_invalid_non_mapping_content(self): - """ - Parses a malformed GETINFO reply containing a line that isn't a key=value - entry. - """ - - control_message = mocking.get_message(NON_KEY_VALUE_ENTRY) - self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message) - - def test_invalid_multiline_content(self): - """ - Parses a malformed GETINFO reply with a multi-line entry missing a newline - between its key and value. This is a proper controller message, but - malformed according to the GETINFO's spec. - """ - - control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE) - self.assertRaises(stem.socket.ProtocolError, stem.control.GetInfoResponse.convert, control_message) - diff --git a/test/unit/response/__init__.py b/test/unit/response/__init__.py new file mode 100644 index 0000000..530274c --- /dev/null +++ b/test/unit/response/__init__.py @@ -0,0 +1,6 @@ +""" +Unit tests for stem.response. +""" + +__all__ = ["getinfo", "protocolinfo"] + diff --git a/test/unit/response/getinfo.py b/test/unit/response/getinfo.py new file mode 100644 index 0000000..5f5862a --- /dev/null +++ b/test/unit/response/getinfo.py @@ -0,0 +1,120 @@ +""" +Unit tests for the stem.response.getinfo.GetInfoResponse class. +""" + +import unittest + +import stem.response +import stem.response.getinfo +import test.mocking as mocking + +EMPTY_RESPONSE = "250 OK" + +SINGLE_RESPONSE = """\ +250-version=0.2.3.11-alpha-dev +250 OK""" + +BATCH_RESPONSE = """\ +250-version=0.2.3.11-alpha-dev +250-address=67.137.76.214 +250-fingerprint=5FDE0422045DF0E1879A3738D09099EB4A0C5BA0 +250 OK""" + +MULTILINE_RESPONSE = """\ +250-version=0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c) +250+config-text= +ControlPort 9051 +DataDirectory /home/atagar/.tor +ExitPolicy reject *:* +Log notice stdout +Nickname Unnamed +ORPort 9050 +. +250 OK""" + +NON_KEY_VALUE_ENTRY = """\ +250-version=0.2.3.11-alpha-dev +250-address 67.137.76.214 +250 OK""" + +MISSING_MULTILINE_NEWLINE = """\ +250+config-text=ControlPort 9051 +DataDirectory /home/atagar/.tor +. +250 OK""" + +class TestGetInfoResponse(unittest.TestCase): + def test_empty_response(self): + """ + Parses a GETINFO reply without options (just calling "GETINFO"). + """ + + control_message = mocking.get_message(EMPTY_RESPONSE) + stem.response.convert("GETINFO", control_message) + + # now this should be a GetInfoResponse (ControlMessage subclass) + self.assertTrue(isinstance(control_message, stem.socket.ControlMessage)) + self.assertTrue(isinstance(control_message, stem.response.getinfo.GetInfoResponse)) + + self.assertEqual({}, control_message.values) + + def test_single_response(self): + """ + Parses a GETINFO reply response for a single parameter. + """ + + control_message = mocking.get_message(SINGLE_RESPONSE) + stem.response.convert("GETINFO", control_message) + self.assertEqual({"version": "0.2.3.11-alpha-dev"}, control_message.values) + + def test_batch_response(self): + """ + Parses a GETINFO reply for muiltiple parameters. + """ + + control_message = mocking.get_message(BATCH_RESPONSE) + stem.response.convert("GETINFO", control_message) + + expected = { + "version": "0.2.3.11-alpha-dev", + "address": "67.137.76.214", + "fingerprint": "5FDE0422045DF0E1879A3738D09099EB4A0C5BA0", + } + + self.assertEqual(expected, control_message.values) + + def test_multiline_response(self): + """ + Parses a GETINFO reply for multiple parameters including a multi-line + value. + """ + + control_message = mocking.get_message(MULTILINE_RESPONSE) + stem.response.convert("GETINFO", control_message) + + expected = { + "version": "0.2.3.11-alpha-dev (git-ef0bc7f8f26a917c)", + "config-text": "\n".join(MULTILINE_RESPONSE.splitlines()[2:8]), + } + + self.assertEqual(expected, control_message.values) + + def test_invalid_non_mapping_content(self): + """ + Parses a malformed GETINFO reply containing a line that isn't a key=value + entry. + """ + + control_message = mocking.get_message(NON_KEY_VALUE_ENTRY) + self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message) + + def test_invalid_multiline_content(self): + """ + Parses a malformed GETINFO reply with a multi-line entry missing a newline + between its key and value. This is a proper controller message, but + malformed according to the GETINFO's spec. + """ + + control_message = mocking.get_message(MISSING_MULTILINE_NEWLINE) + self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "GETINFO", control_message) + diff --git a/test/unit/response/protocolinfo.py b/test/unit/response/protocolinfo.py new file mode 100644 index 0000000..a17c58c --- /dev/null +++ b/test/unit/response/protocolinfo.py @@ -0,0 +1,180 @@ +""" +Unit tests for the stem.response.protocolinfo.ProtocolInfoResponse class. +""" + +import unittest + +from stem.connection import AuthMethod +import stem.socket +import stem.version +import stem.util.proc +import stem.util.system +import stem.response +import stem.response.protocolinfo +import test.mocking as mocking + +NO_AUTH = """250-PROTOCOLINFO 1 +250-AUTH METHODS=NULL +250-VERSION Tor="0.2.1.30" +250 OK""" + +PASSWORD_AUTH = """250-PROTOCOLINFO 1 +250-AUTH METHODS=HASHEDPASSWORD +250-VERSION Tor="0.2.1.30" +250 OK""" + +COOKIE_AUTH = r"""250-PROTOCOLINFO 1 +250-AUTH METHODS=COOKIE COOKIEFILE="/tmp/my data\"dir//control_auth_cookie" +250-VERSION Tor="0.2.1.30" +250 OK""" + +MULTIPLE_AUTH = """250-PROTOCOLINFO 1 +250-AUTH METHODS=COOKIE,HASHEDPASSWORD COOKIEFILE="/home/atagar/.tor/control_auth_cookie" +250-VERSION Tor="0.2.1.30" +250 OK""" + +UNKNOWN_AUTH = """250-PROTOCOLINFO 1 +250-AUTH METHODS=MAGIC,HASHEDPASSWORD,PIXIE_DUST +250-VERSION Tor="0.2.1.30" +250 OK""" + +MINIMUM_RESPONSE = """250-PROTOCOLINFO 5 +250 OK""" + +RELATIVE_COOKIE_PATH = r"""250-PROTOCOLINFO 1 +250-AUTH METHODS=COOKIE COOKIEFILE="./tor-browser_en-US/Data/control_auth_cookie" +250-VERSION Tor="0.2.1.30" +250 OK""" + +class TestProtocolInfoResponse(unittest.TestCase): + def test_convert(self): + """ + Exercises functionality of the convert method both when it works and + there's an error. + """ + + # working case + control_message = mocking.get_message(NO_AUTH) + stem.response.convert("PROTOCOLINFO", control_message) + + # now this should be a ProtocolInfoResponse (ControlMessage subclass) + self.assertTrue(isinstance(control_message, stem.socket.ControlMessage)) + self.assertTrue(isinstance(control_message, stem.response.protocolinfo.ProtocolInfoResponse)) + + # exercise some of the ControlMessage functionality + raw_content = (NO_AUTH + "\n").replace("\n", "\r\n") + self.assertEquals(raw_content, control_message.raw_content()) + self.assertTrue(str(control_message).startswith("PROTOCOLINFO 1")) + + # attempt to convert the wrong type + self.assertRaises(TypeError, stem.response.convert, "PROTOCOLINFO", "hello world") + + # attempt to convert a different message type + bw_event_control_message = mocking.get_message("650 BW 32326 2856") + self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "PROTOCOLINFO", bw_event_control_message) + + def test_no_auth(self): + """ + Checks a response when there's no authentication. + """ + + control_message = mocking.get_message(NO_AUTH) + stem.response.convert("PROTOCOLINFO", control_message) + + self.assertEquals(1, control_message.protocol_version) + self.assertEquals(stem.version.Version("0.2.1.30"), control_message.tor_version) + self.assertEquals((AuthMethod.NONE, ), control_message.auth_methods) + self.assertEquals((), control_message.unknown_auth_methods) + self.assertEquals(None, control_message.cookie_path) + + def test_password_auth(self): + """ + Checks a response with password authentication. + """ + + control_message = mocking.get_message(PASSWORD_AUTH) + stem.response.convert("PROTOCOLINFO", control_message) + self.assertEquals((AuthMethod.PASSWORD, ), control_message.auth_methods) + + def test_cookie_auth(self): + """ + Checks a response with cookie authentication and a path including escape + characters. + """ + + control_message = mocking.get_message(COOKIE_AUTH) + stem.response.convert("PROTOCOLINFO", control_message) + self.assertEquals((AuthMethod.COOKIE, ), control_message.auth_methods) + self.assertEquals("/tmp/my data\"dir//control_auth_cookie", control_message.cookie_path) + + def test_multiple_auth(self): + """ + Checks a response with multiple authentication methods. + """ + + control_message = mocking.get_message(MULTIPLE_AUTH) + stem.response.convert("PROTOCOLINFO", control_message) + self.assertEquals((AuthMethod.COOKIE, AuthMethod.PASSWORD), control_message.auth_methods) + self.assertEquals("/home/atagar/.tor/control_auth_cookie", control_message.cookie_path) + + def test_unknown_auth(self): + """ + Checks a response with an unrecognized authtentication method. + """ + + control_message = mocking.get_message(UNKNOWN_AUTH) + stem.response.convert("PROTOCOLINFO", control_message) + self.assertEquals((AuthMethod.UNKNOWN, AuthMethod.PASSWORD), control_message.auth_methods) + self.assertEquals(("MAGIC", "PIXIE_DUST"), control_message.unknown_auth_methods) + + def test_minimum_response(self): + """ + Checks a PROTOCOLINFO response that only contains the minimum amount of + information to be a valid response. + """ + + control_message = mocking.get_message(MINIMUM_RESPONSE) + stem.response.convert("PROTOCOLINFO", control_message) + + self.assertEquals(5, control_message.protocol_version) + self.assertEquals(None , control_message.tor_version) + self.assertEquals((), control_message.auth_methods) + self.assertEquals((), control_message.unknown_auth_methods) + self.assertEquals(None, control_message.cookie_path) + + def test_relative_cookie(self): + """ + Checks an authentication cookie with a relative path where expansion both + succeeds and fails. + """ + + # we need to mock both pid and cwd lookups since the general cookie + # expanion works by... + # - resolving the pid of the "tor" process + # - using that to get tor's cwd + + def call_mocking(command): + if command == stem.util.system.GET_PID_BY_NAME_PGREP % "tor": + return ["10"] + elif command == stem.util.system.GET_CWD_PWDX % 10: + return ["10: /tmp/foo"] + + mocking.mock(stem.util.proc.is_available, mocking.return_false()) + mocking.mock(stem.util.system.is_available, mocking.return_true()) + mocking.mock(stem.util.system.call, call_mocking) + + control_message = mocking.get_message(RELATIVE_COOKIE_PATH) + stem.response.convert("PROTOCOLINFO", control_message) + self.assertEquals("/tmp/foo/tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path) + + # exercise cookie expansion where both calls fail (should work, just + # leaving the path unexpanded) + + mocking.mock(stem.util.system.call, mocking.return_none()) + control_message = mocking.get_message(RELATIVE_COOKIE_PATH) + stem.response.convert("PROTOCOLINFO", control_message) + self.assertEquals("./tor-browser_en-US/Data/control_auth_cookie", control_message.cookie_path) + + # reset system call mocking + mocking.revert_mocking() +
tor-commits@lists.torproject.org