commit af9e5b4e9f4dcb149d37c7f7ad0249abaa56e689 Author: Damian Johnson atagar@torproject.org Date: Wed Jul 18 09:55:06 2012 -0700
Revised MicrodescriptorExitPolicy
Rewrite the MicrodescriptorExitPolicy and expanded its tests. --- stem/exit_policy.py | 160 ++++++++++++++++++++++----------------- test/unit/exit_policy/policy.py | 89 +++++++++++++++++----- 2 files changed, 160 insertions(+), 89 deletions(-)
diff --git a/stem/exit_policy.py b/stem/exit_policy.py index 8a5f528..49a6cef 100644 --- a/stem/exit_policy.py +++ b/stem/exit_policy.py @@ -15,15 +15,13 @@ exiting to a destination is permissable or not. For instance...
policy = stem.exit_policy.MicrodescriptorExitPolicy("accept 80,443") print policy
accept 80,443 - >>> policy.check("www.google.com", 80) - True - >>> policy.check(80) + >>> policy.check("75.119.206.243", 80) True
::
ExitPolicy - Exit policy for a Tor relay - |- set_default_allowed - sets the default can_exit_to() response if no rules apply + | + MicrodescriptorExitPolicy - Microdescriptor exit policy |- can_exit_to - check if exiting to this destination is allowed or not |- is_exiting_allowed - check if any exiting is allowed |- summary - provides a short label, similar to a microdescriptor @@ -35,12 +33,6 @@ exiting to a destination is permissable or not. For instance... |- is_port_wildcard - checks if we'll accept any port |- is_match - checks if we match a given destination +- __str__ - string representation for this rule - - MicrodescriptorExitPolicy - Microdescriptor exit policy - |- check - check if exiting to this port is allowed - |- ports - returns a list of ports - |- is_accept - check if it's a list of accepted/rejected ports - +- __str__ - return the summary """
import stem.util.connection @@ -236,6 +228,94 @@ class ExitPolicy(object): else: return False
+class MicrodescriptorExitPolicy(ExitPolicy): + """ + Exit policy provided by the microdescriptors. This is a distilled version of + a normal ExitPolicy contains, just consisting of a list of ports that are + either accepted or rejected. For instance... + + :: + + accept 80,443 # only accepts common http ports + reject 1-1024 # only accepts non-privilaged ports + + Since these policies are a subset of the exit policy information (lacking IP + ranges) clients can only use them to guess if a relay will accept traffic or + not. To quote the dir-spec (section 3.2.1)... + + :: + + With microdescriptors, clients don't learn exact exit policies: + clients can only guess whether a relay accepts their request, try the + BEGIN request, and might get end-reason-exit-policy if they guessed + wrong, in which case they'll have to try elsewhere. + + :var set ports: ports that this policy includes + :var bool is_accept: True if these are ports that we accept, False if they're ports that we reject + + :param str policy: policy string that describes this policy + """ + + def __init__(self, policy): + # Microdescriptor policies are of the form... + # + # MicrodescriptrPolicy ::= ("accept" / "reject") SP PortList NL + # PortList ::= PortOrRange + # PortList ::= PortList "," PortOrRange + # PortOrRange ::= INT "-" INT / INT + + self.ports = set() + self._policy = policy + + if policy.startswith("accept"): + self.is_accept = True + elif policy.startswith("reject"): + self.is_accept = False + else: + raise ValueError("A microdescriptor exit policy must start with either 'accept' or 'reject': %s" % policy) + + policy = policy[6:] + + if not policy.startswith(" ") or (len(policy) - 1 != len(policy.lstrip())): + raise ValueError("A microdescriptor exit policy should have a space separating accept/reject from its port list: %s" % self._policy) + + policy = policy[1:] + + # convert our port list into ExitPolicyRules + rules = [] + rule_format = "accept *:%s" if self.is_accept else "reject *:%s" + + for port_entry in policy.split(","): + rule_str = rule_format % port_entry + + try: + rule = ExitPolicyRule(rule_str) + self.ports.update(range(rule.min_port, rule.max_port + 1)) + rules.append(rule) + except ValueError, exc: + exc_msg = "Policy '%s' is malformed. %s" % (self._policy, str(exc).replace(rule_str, port_entry)) + raise ValueError(exc_msg) + + super(MicrodescriptorExitPolicy, self).__init__(*rules) + + def can_exit_to(self, address = None, port = None): + # we can greatly simplify our check since our policies don't concern + # addresses or masks + + if port in self.ports: + return self.is_accept + else: + return not self.is_accept + + def __str__(self): + return self._policy + + def __eq__(self, other): + if isinstance(other, MicrodescriptorExitPolicy): + return str(self) == str(other) + else: + return False + class ExitPolicyRule(object): """ Single rule from the user's exit policy. These rules are chained together to @@ -281,7 +361,7 @@ class ExitPolicyRule(object):
exitpattern = rule[6:]
- if not exitpattern.startswith(" ") or (len(exitpattern) - 1 != len(exitpattern.lstrip())) : + if not exitpattern.startswith(" ") or (len(exitpattern) - 1 != len(exitpattern.lstrip())): raise ValueError("An exit policy should have a space separating its accept/reject from the exit pattern: %s" % rule)
exitpattern = exitpattern[1:] @@ -522,62 +602,4 @@ class ExitPolicyRule(object): return str(self) == str(other) else: return False - -class MicrodescriptorExitPolicy: - """ - Microdescriptor exit policy - 'accept 53,80,443' - """ - - def __init__(self, summary): - self.ports = [] - self.is_accept = None - self.summary = summary - - # sanitize the input a bit, cleaning up tabs and stripping quotes - summary = self.summary.replace("\t", " ").replace(""", "") - - self.is_accept = summary.startswith("accept") - - # strips off "accept " or "reject " and extra spaces - summary = summary[7:].replace(" ", "") - - for ports in summary.split(','): - if '-' in ports: - port_range = ports.split("-", 1) - if not stem.util.connection.is_valid_port(port_range): - raise ValueError("Invaid port range") - self.ports.append(range(int(port_range[2])), int(port_range[1])) - if not stem.util.connection.is_valid_port(ports): - raise ValueError("Invalid port range") - self.ports.append(int(ports)) - - def check(self, ip_address=None, port=None): - # stem is intelligent about the arguments - if not port: - if not '.' in str(ip_address): - port = ip_address - - port = int(port) - - if port in self.ports: - # its a list of accepted ports - if self.is_accept: - return True - else: - return False - else: - # its a list of rejected ports - if not self.is_accept: - return True - else: - return False - - def __str__(self): - return self.summary - - def ports(self): - return self.ports - - def is_accept(self): - return self.is_accept
diff --git a/test/unit/exit_policy/policy.py b/test/unit/exit_policy/policy.py index da4fc96..6ce0e4f 100644 --- a/test/unit/exit_policy/policy.py +++ b/test/unit/exit_policy/policy.py @@ -5,7 +5,9 @@ Unit tests for the stem.exit_policy.ExitPolicy class. import unittest import stem.exit_policy import stem.util.system -from stem.exit_policy import ExitPolicy, ExitPolicyRule +from stem.exit_policy import ExitPolicy, \ + MicrodescriptorExitPolicy, \ + ExitPolicyRule
import test.mocking as mocking
@@ -17,7 +19,8 @@ class TestExitPolicy(unittest.TestCase): self.assertEquals("accept 80, 443", policy.summary()) self.assertTrue(policy.can_exit_to("75.119.206.243", 80))
- # TODO: add MicrodescriptorExitPolicy after it has been revised + policy = MicrodescriptorExitPolicy("accept 80,443") + self.assertTrue(policy.can_exit_to("75.119.206.243", 80))
def test_constructor(self): # The ExitPolicy constructor takes a series of string or ExitPolicyRule @@ -124,25 +127,71 @@ class TestExitPolicy(unittest.TestCase): self.assertEquals(rules, list(ExitPolicy(*rules))) self.assertEquals(rules, list(ExitPolicy('accept *:80', 'accept *:443', 'reject *:*')))
- - def test_microdesc_exit_parsing(self): - microdesc_exit_policy = stem.exit_policy.MicrodescriptorExitPolicy("accept 80,443") - - self.assertEqual(str(microdesc_exit_policy),"accept 80,443") + def test_microdescriptor_parsing(self): + # mapping between inputs and if they should succeed or not + test_inputs = { + 'accept 80': True, + 'accept 80,443': True, + '': False, + 'accept': False, + 'accept ': False, + 'accept\t80,443': False, + 'accept 80, 443': False, + 'accept 80,\t443': False, + '80,443': False, + 'accept 80,-443': False, + 'accept 80,+443': False, + 'accept 80,66666': False, + 'reject 80,foo': False, + 'bar 80,443': False, + }
- self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "accept 80,-443") - self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "accept 80,+443") - self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "accept 80,66666") - self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "reject 80,foo") - self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "bar 80,foo") - self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "foo") - self.assertRaises(ValueError, stem.exit_policy.MicrodescriptorExitPolicy, "bar 80-foo") + for policy_arg, expect_success in test_inputs.items(): + try: + policy = MicrodescriptorExitPolicy(policy_arg) + + if expect_success: + self.assertEqual(policy_arg, str(policy)) + else: + self.fail() + except ValueError: + if expect_success: self.fail() + + def test_microdescriptor_attributes(self): + # checks that its is_accept and ports attributes are properly set + + # single port + policy = MicrodescriptorExitPolicy('accept 443') + self.assertTrue(policy.is_accept) + self.assertEquals(set([443]), policy.ports) + + # multiple ports + policy = MicrodescriptorExitPolicy('accept 80,443') + self.assertTrue(policy.is_accept) + self.assertEquals(set([80, 443]), policy.ports) + + # port range + policy = MicrodescriptorExitPolicy('reject 1-1024') + self.assertFalse(policy.is_accept) + self.assertEquals(set(range(1, 1025)), policy.ports) + + def test_microdescriptor_can_exit_to(self): + test_inputs = { + 'accept 443': {442: False, 443: True, 444: False}, + 'reject 443': {442: True, 443: False, 444: True}, + 'accept 80,443': {80: True, 443: True, 10: False}, + 'reject 1-1024': {1: False, 1024: False, 1025: True}, + }
- def test_micodesc_exit_check(self): - microdesc_exit_policy = stem.exit_policy.MicrodescriptorExitPolicy("accept 80,443") + for policy_arg, attr in test_inputs.items(): + policy = MicrodescriptorExitPolicy(policy_arg) + + for port, expected_value in attr.items(): + self.assertEqual(expected_value, policy.can_exit_to(port = port))
- self.assertTrue(microdesc_exit_policy.check(80)) - self.assertTrue(microdesc_exit_policy.check("www.atagar.com", 443)) + # address argument should be ignored + policy = MicrodescriptorExitPolicy('accept 80,443')
- self.assertFalse(microdesc_exit_policy.check(22)) - self.assertFalse(microdesc_exit_policy.check("www.atagar.com", 8118)) + self.assertFalse(policy.can_exit_to('blah', 79)) + self.assertTrue(policy.can_exit_to('blah', 80)) +
tor-commits@lists.torproject.org