commit 79fc8cd7514cda072abf855b6a1908f0dd10bb17 Author: Damian Johnson atagar@torproject.org Date: Sat Dec 29 23:27:06 2012 -0800
Revisions for prior mocking and controller changes
Changes include...
* Rewrite of get_socks_ports() with the following changes...
* Renaming to get_socks_listeners() since it provides (address, port) tuples, not just ports.
* Better error handling. Previously malformed content such as invalid ports would cause us to raise ValueErrors.
* The method wasn't documented as raising any exceptions, but if the get_conf() calls fail then a ControllerError will be raised.
* Minor corrections to the return_for_args() examples.
* Renamed return_for_args()'s "method" arg to "is_method" (it's nice when the an arg name can imply the type).
* I'm not sure what was meant by "Also, remove the the space in the join so that there is only one parameter passed to the string substitution.". Personally I'd find a message like...
"Unrecognized argument sent for return_for_args(). Got 'foo, bar' but we only recognize 'foo, blarg'."
... to be a bit nicer than...
"Unrecognized argument sent for return_for_args(). Got 'foo, bar' but we only recognize 'foo;blarg'." --- stem/control.py | 53 ++++++++++++++++++--------- test/integ/control/controller.py | 2 +- test/mocking.py | 68 +++++++++++++++++------------------ test/unit/control/controller.py | 72 +++++++++++++++++++++++++++++-------- test/unit/tutorial.py | 2 +- 5 files changed, 126 insertions(+), 71 deletions(-)
diff --git a/stem/control.py b/stem/control.py index 8a36285..704bb5e 100644 --- a/stem/control.py +++ b/stem/control.py @@ -26,6 +26,7 @@ providing its own for interacting at a higher level. |- set_options - sets or resets the values of multiple configuration options |- load_conf - loads configuration information as if it was in the torrc |- save_conf - saves configuration information to the torrc + |- get_socks_listeners - provides (address, port) where tor is listening for SOCKS connections |- is_feature_enabled - checks if a given controller feature is enabled |- enable_feature - enables a controller feature that has been disabled by default |- signal - sends a signal to the tor client @@ -1320,32 +1321,48 @@ class Controller(BaseController): else: raise stem.ProtocolError("SAVECONF returned unexpected response code")
- def get_socks_ports(self): + def get_socks_listeners(self): """ - Returns a list of SOCKS proxy ports open on the controlled tor instance. + Provides the SOCKS **(address, port)** tuples that tor has open.
- :returns: list of **(host, port)** tuples or an empty list if there are no - SOCKS listeners + :returns: list of **(address, port)** tuples for the available SOCKS + listeners + + :raises: :class:`stem.ControllerError` if unable to determine the listeners """
+ proxy_addrs = [] + try: - raw_addrs = self.get_info("net/listeners/socks").split() - # remove the surrounding quotes from each listener - raw_addrs = [x.replace(""", "") for x in raw_addrs] + for listener in self.get_info("net/listeners/socks").split(): + if not (listener.startswith('"') and listener.endswith('"')): + raise stem.ProtocolError("'GETINFO net/listeners/socks' responses are expected to be quoted: %s" % listener) + elif not ':' in listener: + raise stem.ProtocolError("'GETINFO net/listeners/socks' had a listener without a colon: %s" % listener) + + listener = listener[1:-1] # strip quotes + addr, port = listener.split(':') + proxy_addrs.append((addr, port)) except stem.InvalidArguments: - # tor version is old (pre-tor-0.2.2.26-beta); use get_conf() + # tor version is old (pre-tor-0.2.2.26-beta), use get_conf() instead socks_port = self.get_conf('SocksPort') - raw_addrs = [] + for listener in self.get_conf('SocksListenAddress', multiple = True): - if listener.count(':') == 0: - listener = listener + ":" + socks_port - raw_addrs.append(listener) - # both processes above give a list of strings of the form host:port - proxy_addrs = [] - for proxy in raw_addrs: - proxy_pair = proxy.split(":") - proxy_addrs.append(tuple((proxy_pair[0], int(proxy_pair[1])))) - return proxy_addrs + if ':' in listener: + addr, port = listener.split(':') + proxy_addrs.append((addr, port)) + else: + proxy_addrs.append((listener, socks_port)) + + # validate that address/ports are valid, and convert ports to ints + + for addr, port in proxy_addrs: + if not stem.util.connection.is_valid_ip_address(addr): + raise stem.ProtocolError("Invalid address for a SOCKS listener: %s" % addr) + elif not stem.util.connection.is_valid_port(port): + raise stem.ProtocolError("Invalid port for a SOCKS listener: %s" % port) + + return [(addr, int(port)) for (addr, port) in proxy_addrs]
def is_feature_enabled(self, feature): """ diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index 5c146e0..86056aa 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -467,7 +467,7 @@ class TestController(unittest.TestCase): runner = test.runner.get_runner()
with runner.get_tor_controller() as controller: - self.assertEqual([('127.0.0.1', 1112)], controller.get_socks_ports()) + self.assertEqual([('127.0.0.1', 1112)], controller.get_socks_listeners())
def test_enable_feature(self): """ diff --git a/test/mocking.py b/test/mocking.py index 074c977..a86b958 100644 --- a/test/mocking.py +++ b/test/mocking.py @@ -204,63 +204,61 @@ def return_true(): return return_value(True) def return_false(): return return_value(False) def return_none(): return return_value(None)
-def return_for_args(args_to_return_value, default = None, method = False): +def return_for_args(args_to_return_value, default = None, is_method = False): """ Returns a value if the arguments to it match something in a given 'argument => return value' mapping. Otherwise, a default function is called with the arguments.
- The mapped argument is a tuple (not a list) of parameters to a function (or - method). Positional arguments must be in the order used to call the mocked - function and keyword arguments must be strings of the form 'k=v' in - alphabetical order. Some examples: + The mapped argument is a tuple (not a list) of parameters to a function or + method. Positional arguments must be in the order used to call the mocked + function, and keyword arguments must be strings of the form 'k=v'. Keyword + arguments **must** appear in alphabetical order. For example...
::
- Mocked functions can return different types depending on input: + mocking.mock("get_answer", mocking.return_for_args({ + ("breakfast_menu",): "spam", + ("lunch_menu",): "eggs and spam", + (42,): ["life", "universe", "everything"], + }))
- mocking.mock("get_answer", mocking.return_for_args({ - ("breakfast_menu",): "spam", - ("lunch_menu",): "eggs and spam", - (42,): ["life", "universe", "everything"], - }) - - mocking.mock("align_text", { - ("Stem", "alignment=left", "size=10"): "Stem ", - ("Stem", "alignment=center", "size=10"): " Stem ", - ("Stem", "alignment=right", "size=10"): " Stem", - }) - - The mocked method returns one of three circuit ids depending on the input: - - :: + mocking.mock("align_text", mocking.return_for_args({ + ("Stem", "alignment=left", "size=10"): "Stem ", + ("Stem", "alignment=center", "size=10"): " Stem ", + ("Stem", "alignment=right", "size=10"): " Stem", + }))
- mocking.mock_method(Controller, "new_circuit", mocking.return_for_args({ - (): "1", - ("path=['718BCEA286B531757ACAFF93AE04910EA73DE617', " + \ - "'30BAB8EE7606CBD12F3CC269AE976E0153E7A58D', " + \ - "'2765D8A8C4BBA3F89585A9FFE0E8575615880BEB']",): "2" - ("path=['1A', '2B', '3C']", "purpose=controller"): "3" - }, method = True) + mocking.mock_method(Controller, "new_circuit", mocking.return_for_args({ + (): "1", + ("path=['718BCEA286B531757ACAFF93AE04910EA73DE617', " + \ + "'30BAB8EE7606CBD12F3CC269AE976E0153E7A58D', " + \ + "'2765D8A8C4BBA3F89585A9FFE0E8575615880BEB']",): "2" + ("path=['1A', '2B', '3C']", "purpose=controller"): "3" + }, is_method = True))
- :param dict,tuple args_to_return_value: mapping of arguments to the value we should provide - :param functor default: returns the value of this function if the args don't match something that we have, we raise a ValueError by default - :param bool method: removes the 'self' reference before processing the remainder of the parameters + :param dict args_to_return_value: mapping of arguments to the value we should provide + :param functor default: returns the value of this function if the args don't + match something that we have, we raise a ValueError by default + :param bool is_method: handles this like a method, removing the 'self' + reference """
def _return_value(*args, **kwargs): - # strip off the 'self' for mock classes - if args and method: + # strip off the 'self' if we're mocking a method + if args and is_method: args = args[1:] if len(args) > 2 else [args[1]]
if kwargs: - args.extend(['='.join((str(k),str(kwargs[k]))) for k in sorted(kwargs.keys())]) + args.extend(["%s=%s" % (k, kwargs[k]) for k in sorted(kwargs.keys())]) + args = tuple(args) + if args in args_to_return_value: return args_to_return_value[args] elif default is None: arg_label = ", ".join([str(v) for v in args]) - arg_keys = ";".join([str(v) for v in args_to_return_value.keys()]) + arg_keys = ", ".join([str(v) for v in args_to_return_value.keys()]) raise ValueError("Unrecognized argument sent for return_for_args(). Got '%s' but we only recognize '%s'." % (arg_label, arg_keys)) else: return default(args) diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py index f3849eb..695c2de 100644 --- a/test/unit/control/controller.py +++ b/test/unit/control/controller.py @@ -77,54 +77,94 @@ class TestControl(unittest.TestCase): # EventType.SIGNAL was added in tor version 0.2.3.1-alpha self.assertRaises(InvalidRequest, self.controller.add_event_listener, mocking.no_op(), EventType.SIGNAL)
- def test_socks_port_old_tor(self): + def test_get_socks_listeners_old(self): """ - Exercises the get_socks_ports method as if talking to an old tor process. + Exercises the get_socks_listeners() method as though talking to an old tor + instance. """
# An old tor raises stem.InvalidArguments for get_info about socks, but - # get_socks_ports returns the socks information, anyway. + # get_socks_listeners should work anyway. + mocking.mock_method(Controller, "get_info", mocking.raise_exception(InvalidArguments)) + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ ("SocksPort",): "9050", ("SocksListenAddress", "multiple=True"): ["127.0.0.1"] - }, method = True)) - self.assertEqual([('127.0.0.1', 9050)], self.controller.get_socks_ports()) + }, is_method = True)) + self.assertEqual([('127.0.0.1', 9050)], self.controller.get_socks_listeners())
# Again, an old tor, but SocksListenAddress overrides the port number. + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ ("SocksPort",): "9050", ("SocksListenAddress", "multiple=True"): ["127.0.0.1:1112"] - }, method = True)) - self.assertEqual([('127.0.0.1', 1112)], self.controller.get_socks_ports()) + }, is_method = True)) + self.assertEqual([('127.0.0.1', 1112)], self.controller.get_socks_listeners())
# Again, an old tor, but multiple listeners + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ ("SocksPort",): "9050", ("SocksListenAddress", "multiple=True"): ["127.0.0.1:1112", "127.0.0.1:1114"] - }, method = True)) - self.assertEqual([('127.0.0.1', 1112), ('127.0.0.1', 1114)], self.controller.get_socks_ports()) + }, is_method = True)) + self.assertEqual([('127.0.0.1', 1112), ('127.0.0.1', 1114)], self.controller.get_socks_listeners())
# Again, an old tor, but no SOCKS listeners + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ ("SocksPort",): "0", ("SocksListenAddress", "multiple=True"): [] - }, method = True)) - self.assertEqual([], self.controller.get_socks_ports()) + }, is_method = True)) + self.assertEqual([], self.controller.get_socks_listeners()) + + # Where tor provides invalid ports or addresses + + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ + ("SocksPort",): "blarg", + ("SocksListenAddress", "multiple=True"): ["127.0.0.1"] + }, is_method = True)) + self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners) + + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ + ("SocksPort",): "0", + ("SocksListenAddress", "multiple=True"): ["127.0.0.1:abc"] + }, is_method = True)) + self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners) + + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ + ("SocksPort",): "40", + ("SocksListenAddress", "multiple=True"): ["500.0.0.1"] + }, is_method = True)) + self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
- def test_socks_port_new_tor(self): + def test_get_socks_listeners_new(self): """ - Exercises the get_socks_ports method as if talking to a newer tor process. + Exercises the get_socks_listeners() method as if talking to a newer tor + instance. """
# multiple SOCKS listeners mocking.mock_method(Controller, "get_info", mocking.return_value( - ""127.0.0.1:1112" "127.0.0.1:1114"" + '"127.0.0.1:1112" "127.0.0.1:1114"' )) self.assertEqual([('127.0.0.1', 1112), ('127.0.0.1', 1114)], - self.controller.get_socks_ports()) + self.controller.get_socks_listeners())
# no SOCKS listeners mocking.mock_method(Controller, "get_info", mocking.return_value("")) - self.assertEqual([], self.controller.get_socks_ports()) + self.assertEqual([], self.controller.get_socks_listeners()) + + # check where GETINFO provides malformed content + + invalid_responses = ( + '"127.0.0.1"', # address only + '"1112"', # port only + '"5127.0.0.1:1112"', # invlaid address + '"127.0.0.1:991112"', # invalid port + ) + + for response in invalid_responses: + mocking.mock_method(Controller, "get_info", mocking.return_value(response)) + self.assertRaises(stem.ProtocolError, self.controller.get_socks_listeners)
diff --git a/test/unit/tutorial.py b/test/unit/tutorial.py index b586549..79baa8c 100644 --- a/test/unit/tutorial.py +++ b/test/unit/tutorial.py @@ -21,7 +21,7 @@ class TestTutorial(unittest.TestCase): 'get_info': mocking.return_for_args({ ('traffic/read',): '1234', ('traffic/written',): '5678', - }, method = True), + }, is_method = True), })
controller.authenticate()