commit 716f8a693e9b814da5e2c9df551dbe6768f4f324 Author: Damian Johnson atagar@torproject.org Date: Sun Jan 13 17:19:33 2013 -0800
Controller method to get the exit policy
What started out with 'we should have a method to get our exit policy' turned out to not be as simple as I thought. A relay's exit policy is really the combination of three things:
* 'reject private:*, reject [public address]:*' if ExitPolicyRejectPrivate is set.
* Our ExitPolicy. Tor provides the torrc value rather than a proper policy spec, so I added a get_config_policy() function to account for the differences.
* The default exit policy (as per 'GETINFO exit-policy/default'). --- stem/control.py | 51 +++++++++++++++++++++++++++++++++++++- stem/exit_policy.py | 2 + test/integ/control/controller.py | 35 ++++++++++++++++++++++++++ test/unit/control/controller.py | 43 ++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletions(-)
diff --git a/stem/control.py b/stem/control.py index 4d3bb52..2090983 100644 --- a/stem/control.py +++ b/stem/control.py @@ -16,7 +16,8 @@ providing its own for interacting at a higher level. |- authenticate - authenticates this controller with tor | |- get_info - issues a GETINFO query for a parameter - |- get_version - convenience method to get tor version + |- get_version - provides our tor version + |- get_exit_policy - provides our exit policy |- get_socks_listeners - provides where tor is listening for SOCKS connections |- get_protocolinfo - information about the controller interface | @@ -135,6 +136,7 @@ import time
import stem.descriptor.router_status_entry import stem.descriptor.server_descriptor +import stem.exit_policy import stem.response import stem.socket import stem.util.connection @@ -813,6 +815,50 @@ class Controller(BaseController): else: return default
+ def get_exit_policy(self, default = UNDEFINED): + """ + Effective ExitPolicy for our relay. This accounts for + ExitPolicyRejectPrivate and default policies. + + :param object default: response if the query fails + + :returns: :class:`~stem.exit_policy.ExitPolicy` of the tor instance that + we're connected to + + :raises: + * :class:`stem.ControllerError` if unable to query the policy + * **ValueError** if unable to parse the policy + + An exception is only raised if we weren't provided a default response. + """ + + with self._msg_lock: + try: + if not "exit_policy" in self._request_cache: + policy = [] + + if self.get_conf("ExitPolicyRejectPrivate") == "1": + policy.append("reject private:*") + + public_addr = self.get_info("address", None) + + if public_addr: + policy.append("reject %s:*" % public_addr) + + for policy_line in self.get_conf("ExitPolicy", multiple = True): + policy += policy_line.split(",") + + policy += self.get_info("exit-policy/default").split(",") + + self._request_cache["exit_policy"] = stem.exit_policy.get_config_policy(policy) + + return self._request_cache["exit_policy"] + except Exception, exc: + if default == UNDEFINED: + raise exc + else: + return default + def get_socks_listeners(self, default = UNDEFINED): """ Provides the SOCKS **(address, port)** tuples that tor has open. @@ -1312,6 +1358,9 @@ class Controller(BaseController): self._request_cache[cache_key] = [value] else: self._request_cache[cache_key] = value + + if param.lower() == "exitpolicy" and "exit_policy" in self._request_cache: + del self._request_cache["exit_policy"] else: log.debug("%s (failed, code: %s, message: %s)" % (query, response.code, response.message))
diff --git a/stem/exit_policy.py b/stem/exit_policy.py index 2809e5e..e568deb 100644 --- a/stem/exit_policy.py +++ b/stem/exit_policy.py @@ -38,6 +38,8 @@ exiting to a destination is permissible or not. For instance... |- get_masked_bits - provides the bit representation of our mask +- __str__ - string representation for this rule
+ get_config_policy - provides the ExitPolicy based on torrc rules + .. data:: AddressType (enum)
Enumerations for IP address types that can be in an exit policy. diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index bf6b229..8b75c6f 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -23,6 +23,7 @@ import test.runner import test.util
from stem.control import EventType +from stem.exit_policy import ExitPolicy from stem.version import Requirement
@@ -247,6 +248,40 @@ class TestController(unittest.TestCase): self.assertTrue(isinstance(version, stem.version.Version)) self.assertEqual(version, runner.get_tor_version())
+ def test_get_exit_policy(self): + """ + Sanity test for get_exit_policy(). We have the default policy (no + ExitPolicy set) which is a little... long due to the boilerplate. + """ + + if test.runner.require_control(self): + return + + expected = ExitPolicy( + 'reject 0.0.0.0/8:*', + 'reject 169.254.0.0/16:*', + 'reject 127.0.0.0/8:*', + 'reject 192.168.0.0/16:*', + 'reject 10.0.0.0/8:*', + 'reject 172.16.0.0/12:*', + 'reject *:25', + 'reject *:119', + 'reject *:135-139', + 'reject *:445', + 'reject *:563', + 'reject *:1214', + 'reject *:4661-4666', + 'reject *:6346-6429', + 'reject *:6699', + 'reject *:6881-6999', + 'accept *:*', + ) + + runner = test.runner.get_runner() + + with runner.get_tor_controller() as controller: + self.assertEqual(expected, controller.get_exit_policy()) + def test_authenticate(self): """ Test that the convenient method authenticate() works. diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py index 09b0684..77c8f84 100644 --- a/test/unit/control/controller.py +++ b/test/unit/control/controller.py @@ -11,6 +11,7 @@ import stem.version
from stem import InvalidArguments, InvalidRequest, ProtocolError from stem.control import _parse_circ_path, Controller, EventType +from stem.exit_policy import ExitPolicy from test import mocking
@@ -69,6 +70,48 @@ class TestControl(unittest.TestCase): # Turn caching back on before we leave. self.controller._is_caching_enabled = True
+ def test_get_exit_policy(self): + """ + Exercises the get_exit_policy() method. + """ + + mocking.mock_method(Controller, "get_conf", mocking.return_for_args({ + ("ExitPolicyRejectPrivate",): "1", + ("ExitPolicy", "multiple=True"): ["accept *:80, accept *:443", "accept 43.5.5.5,reject *:22"] + }, is_method = True)) + + mocking.mock_method(Controller, "get_info", mocking.return_for_args({ + ("address", None): "123.45.67.89", + ("exit-policy/default",): "reject *:25,reject *:119,reject *:135-139,reject *:445,reject *:563,reject *:1214,reject *:4661-4666,reject *:6346-6429,reject *:6699,reject *:6881-6999,accept *:*" + }, is_method = True)) + + expected = ExitPolicy( + 'reject 0.0.0.0/8:*', # private entries + 'reject 169.254.0.0/16:*', + 'reject 127.0.0.0/8:*', + 'reject 192.168.0.0/16:*', + 'reject 10.0.0.0/8:*', + 'reject 172.16.0.0/12:*', + 'reject 123.45.67.89:*', # relay's public address + 'accept *:80', # finally we get to our ExitPolicy + 'accept *:443', + 'accept 43.5.5.5:*', + 'reject *:22', + 'reject *:25', # default policy + 'reject *:119', + 'reject *:135-139', + 'reject *:445', + 'reject *:563', + 'reject *:1214', + 'reject *:4661-4666', + 'reject *:6346-6429', + 'reject *:6699', + 'reject *:6881-6999', + 'accept *:*', + ) + + self.assertEqual(expected, self.controller.get_exit_policy()) + def test_get_socks_listeners_old(self): """ Exercises the get_socks_listeners() method as though talking to an old tor