[tor-commits] [stem/master] Revised MicrodescriptorExitPolicy

atagar at torproject.org atagar at torproject.org
Thu Jul 19 16:01:03 UTC 2012


commit af9e5b4e9f4dcb149d37c7f7ad0249abaa56e689
Author: Damian Johnson <atagar at 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))
+





More information about the tor-commits mailing list