[tor-commits] [stem/master] Add ExitPolicy, ExitPolicyIterator from arm/torTools

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


commit 88c436276f1aa0d20206499099b689eb50ace573
Author: Sathyanarayanan Gunasekaran <gsathya.ceg at gmail.com>
Date:   Thu Mar 22 23:20:43 2012 +0530

    Add ExitPolicy, ExitPolicyIterator from arm/torTools
---
 stem/exit_policy.py |  237 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 237 insertions(+), 0 deletions(-)

diff --git a/stem/exit_policy.py b/stem/exit_policy.py
new file mode 100644
index 0000000..40cad39
--- /dev/null
+++ b/stem/exit_policy.py
@@ -0,0 +1,237 @@
+import re
+
+class ExitPolicyIterator:
+  """
+  Basic iterator for cycling through ExitPolicy entries.
+  """
+  
+  def __init__(self, head):
+    self.head = head
+  
+  def next(self):
+    if self.head:
+      lastHead = self.head
+      self.head = self.head.nextRule
+      return lastHead
+    else: raise StopIteration
+
+class ExitPolicy:
+  """
+  Single rule from the user's exit policy. These are chained together to form
+  complete policies.
+  """
+  
+  def __init__(self, ruleEntry, nextRule):
+    """
+    Exit policy rule constructor.
+    
+    Arguments:
+      ruleEntry - tor exit policy rule (for instance, "reject *:135-139")
+      nextRule  - next rule to be checked when queries don't match this policy
+    """
+    
+    # cached summary string
+    self.summaryStr = None
+    
+    # sanitize the input a bit, cleaning up tabs and stripping quotes
+    ruleEntry = ruleEntry.replace("\\t", " ").replace("\"", "")
+    
+    self.ruleEntry = ruleEntry
+    self.nextRule = nextRule
+    self.isAccept = ruleEntry.startswith("accept")
+    
+    # strips off "accept " or "reject " and extra spaces
+    ruleEntry = ruleEntry[7:].replace(" ", "")
+    
+    # split ip address (with mask if provided) and port
+    if ":" in ruleEntry: entryIp, entryPort = ruleEntry.split(":", 1)
+    else: entryIp, entryPort = ruleEntry, "*"
+    
+    # sets the ip address component
+    self.isIpWildcard = entryIp == "*" or entryIp.endswith("/0")
+    
+    # checks for the private alias (which expands this to a chain of entries)
+    if entryIp.lower() == "private":
+      entryIp = PRIVATE_IP_RANGES[0]
+      
+      # constructs the chain backwards (last first)
+      lastHop = self.nextRule
+      prefix = "accept " if self.isAccept else "reject "
+      suffix = ":" + entryPort
+      for addr in PRIVATE_IP_RANGES[-1:0:-1]:
+        lastHop = ExitPolicy(prefix + addr + suffix, lastHop)
+      
+      self.nextRule = lastHop # our next hop is the start of the chain
+    
+    if "/" in entryIp:
+      ipComp = entryIp.split("/", 1)
+      self.ipAddress = ipComp[0]
+      self.ipMask = int(ipComp[1])
+    else:
+      self.ipAddress = entryIp
+      self.ipMask = 32
+    
+    # constructs the binary address just in case of comparison with a mask
+    if self.ipAddress != "*":
+      self.ipAddressBin = ""
+      for octet in self.ipAddress.split("."):
+        # Converts the int to a binary string, padded with zeros. Source:
+        # http://www.daniweb.com/code/snippet216539.html
+        self.ipAddressBin += "".join([str((int(octet) >> y) & 1) for y in range(7, -1, -1)])
+    else:
+      self.ipAddressBin = "0" * 32
+    
+    # sets the port component
+    self.minPort, self.maxPort = 0, 0
+    self.isPortWildcard = entryPort == "*"
+    
+    if entryPort != "*":
+      if "-" in entryPort:
+        portComp = entryPort.split("-", 1)
+        self.minPort = int(portComp[0])
+        self.maxPort = int(portComp[1])
+      else:
+        self.minPort = int(entryPort)
+        self.maxPort = int(entryPort)
+    
+    # if both the address and port are wildcards then we're effectively the
+    # last entry so cut off the remaining chain
+    if self.isIpWildcard and self.isPortWildcard:
+      self.nextRule = None
+  
+  def isExitingAllowed(self):
+    """
+    Provides true if the policy allows exiting whatsoever, false otherwise.
+    """
+    
+    if self.isAccept: return True
+    elif self.isIpWildcard and self.isPortWildcard: return False
+    elif not self.nextRule: return False # fell off policy (shouldn't happen)
+    else: return self.nextRule.isExitingAllowed()
+  
+  def check(self, ipAddress, port):
+    """
+    Checks if the rule chain allows exiting to this address, returning true if
+    so and false otherwise.
+    """
+    
+    port = int(port)
+    
+    # does the port check first since comparing ip masks is more work
+    isPortMatch = self.isPortWildcard or (port >= self.minPort and port <= self.maxPort)
+    
+    if isPortMatch:
+      isIpMatch = self.isIpWildcard or self.ipAddress == ipAddress
+      
+      # expands the check to include the mask if it has one
+      if not isIpMatch and self.ipMask != 32:
+        inputAddressBin = ""
+        for octet in ipAddress.split("."):
+          inputAddressBin += "".join([str((int(octet) >> y) & 1) for y in range(7, -1, -1)])
+        
+        isIpMatch = self.ipAddressBin[:self.ipMask] == inputAddressBin[:self.ipMask]
+      
+      if isIpMatch: return self.isAccept
+    
+    # our policy doesn't concern this address, move on to the next one
+    if self.nextRule: return self.nextRule.check(ipAddress, port)
+    else: return True # fell off the chain without a conclusion (shouldn't happen...)
+  
+  def getSummary(self):
+    """
+    Provides a summary description of the policy chain similar to the
+    consensus. This excludes entries that don't cover all ips, and is either
+    a whitelist or blacklist policy based on the final entry. For instance...
+    accept 80, 443        # just accepts ports 80/443
+    reject 1-1024, 5555   # just accepts non-privilaged ports, excluding 5555
+    """
+    
+    if not self.summaryStr:
+      # determines if we're a whitelist or blacklist
+      isWhitelist = False # default in case we don't have a catch-all policy at the end
+      
+      for rule in self:
+        if rule.isIpWildcard and rule.isPortWildcard:
+          isWhitelist = not rule.isAccept
+          break
+      
+      # Iterates over the rules and adds the the ports we'll return (ie, allows
+      # if a whitelist and rejects if a blacklist). Reguardless of a port's
+      # allow/reject policy, all further entries with that port are ignored since
+      # policies respect the first matching rule.
+      
+      displayPorts, skipPorts = [], []
+      
+      for rule in self:
+        if not rule.isIpWildcard: continue
+        
+        if rule.minPort == rule.maxPort:
+          portRange = [rule.minPort]
+        else:
+          portRange = range(rule.minPort, rule.maxPort + 1)
+        
+        for port in portRange:
+          if port in skipPorts: continue
+          
+          # if accept + whitelist or reject + blacklist then add
+          if rule.isAccept == isWhitelist:
+            displayPorts.append(port)
+          
+          # all further entries with this port are to be ignored
+          skipPorts.append(port)
+      
+      # gets a list of the port ranges
+      if displayPorts:
+        displayRanges, tmpRange = [], []
+        displayPorts.sort()
+        displayPorts.append(None) # ending item to include last range in loop
+        
+        for port in displayPorts:
+          if not tmpRange or tmpRange[-1] + 1 == port:
+            tmpRange.append(port)
+          else:
+            if len(tmpRange) > 1:
+              displayRanges.append("%i-%i" % (tmpRange[0], tmpRange[-1]))
+            else:
+              displayRanges.append(str(tmpRange[0]))
+            
+            tmpRange = [port]
+      else:
+        # everything for the inverse
+        isWhitelist = not isWhitelist
+        displayRanges = ["1-65535"]
+      
+      # constructs the summary string
+      labelPrefix = "accept " if isWhitelist else "reject "
+      
+      self.summaryStr = (labelPrefix + ", ".join(displayRanges)).strip()
+    
+    return self.summaryStr
+  
+  def __iter__(self):
+    return ExitPolicyIterator(self)
+  
+  def __str__(self):
+    # This provides the actual policy rather than the entry used to construct
+    # it so the 'private' keyword is expanded.
+    
+    acceptanceLabel = "accept" if self.isAccept else "reject"
+    
+    if self.isIpWildcard:
+      ipLabel = "*"
+    elif self.ipMask != 32:
+      ipLabel = "%s/%i" % (self.ipAddress, self.ipMask)
+    else: ipLabel = self.ipAddress
+    
+    if self.isPortWildcard:
+      portLabel = "*"
+    elif self.minPort != self.maxPort:
+      portLabel = "%i-%i" % (self.minPort, self.maxPort)
+    else: portLabel = str(self.minPort)
+    
+    myPolicy = "%s %s:%s" % (acceptanceLabel, ipLabel, portLabel)
+    
+    if self.nextRule:
+      return myPolicy + ", " + str(self.nextRule)
+    else: return myPolicy
+





More information about the tor-commits mailing list