[tor-commits] [stem/master] Server descriptor reformatting

atagar at torproject.org atagar at torproject.org
Sun Apr 15 02:50:20 UTC 2012


commit 57ed21a26d440d32e01924cbc027d97665fe8dbc
Author: Damian Johnson <atagar at torproject.org>
Date:   Tue Apr 10 09:03:39 2012 -0700

    Server descriptor reformatting
    
    Cleaning up the recent changes to split apart relay and bridge descriptors,
    dropping an unnecessary parser class and rearranging things to be more
    readable.
---
 stem/descriptor/server_descriptor.py |  204 +++++++++++++++++++---------------
 1 files changed, 112 insertions(+), 92 deletions(-)

diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index a0f688c..8548cf2 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -238,31 +238,28 @@ class ServerDescriptorV3(stem.descriptor.Descriptor):
     self.read_history = None
     self.write_history = None
     self.eventdns = True
+    self._unrecognized_lines = []
+    
+    self._annotation_lines = annotations if annotations else []
+    self._annotation_dict = None # cached breakdown of key/value mappings
     
     # TODO: Until we have a proper ExitPolicy class this is just a list of the
     # exit policy strings...
     
     self.exit_policy = []
     
-    self._unrecognized_lines = []
+    # A descriptor contains a series of 'keyword lines' which are simply a
+    # keyword followed by an optional value. Lines can also be followed by a
+    # signature block.
+    #
+    # We care about the ordering of 'accept' and 'reject' entries because this
+    # influences the resulting exit policy, but for everything else the order
+    # does not matter so breaking it into key / value pairs.
     
-    if annotations:
-      self._annotation_lines = annotations
-      self._annotation_dict = {}
-      
-      for line in annotations:
-        if " " in line:
-          key, value = line.split(" ", 1)
-          self._annotation_dict[key] = value
-        else: self._annotation_dict[line] = None
-    else:
-      self._annotation_lines = []
-      self._annotation_dict = {}
-    
-    contents = _DescriptorContents(raw_contents, validate)
-    self.exit_policy = contents.exit_policy
-    self._parse(contents.entries, validate)
-    if validate: self._check_constraints(contents)
+    entries, first_keyword, last_keyword, self.exit_policy = \
+      _get_descriptor_components(raw_contents, validate)
+    self._parse(entries, validate)
+    if validate: self._check_constraints(entries, first_keyword, last_keyword)
   
   def get_unrecognized_lines(self):
     return list(self._unrecognized_lines)
@@ -279,6 +276,17 @@ class ServerDescriptorV3(stem.descriptor.Descriptor):
       dict with the key/value pairs in our annotations
     """
     
+    if self._annotation_dict == None:
+      annotation_dict = {}
+      
+      for line in self._annotation_lines:
+        if " " in line:
+          key, value = line.split(" ", 1)
+          annotation_dict[key] = value
+        else: annotation_dict[line] = None
+      
+      self._annotation_dict = annotation_dict
+    
     return self._annotation_dict
   
   def get_annotation_lines(self):
@@ -306,7 +314,6 @@ class ServerDescriptorV3(stem.descriptor.Descriptor):
       ValueError if an error occures in validation
     """
     
-    
     for keyword, values in entries.items():
       # most just work with the first (and only) value
       value, block_contents = values[0]
@@ -460,13 +467,15 @@ class ServerDescriptorV3(stem.descriptor.Descriptor):
       else:
         self._unrecognized_lines.append(line)
   
-  def _check_constraints(self, contents):
+  def _check_constraints(self, entries, first_keyword, last_keyword):
     """
     Does a basic check that the entries conform to this descriptor type's
     constraints.
     
     Arguments:
-      contents (_DescriptorContents) - contents to be validated
+      entries (dict)      - keyword => (value, pgp key) entries
+      first_keyword (str) - keyword of the first line
+      last_keyword (str)  - keyword of the last line
     
     Raises:
       ValueError if an issue arises in validation
@@ -475,21 +484,21 @@ class ServerDescriptorV3(stem.descriptor.Descriptor):
     required_fields = self._required_fields()
     if required_fields:
       for keyword in required_fields:
-        if not keyword in contents.entries:
+        if not keyword in entries:
           raise ValueError("Descriptor must have a '%s' entry" % keyword)
     
     single_fields = self._single_fields()
     if single_fields:
       for keyword in self._single_fields():
-        if keyword in contents.entries and len(contents.entries[keyword]) > 1:
+        if keyword in entries and len(entries[keyword]) > 1:
           raise ValueError("The '%s' entry can only appear once in a descriptor" % keyword)
     
-    first_keyword = self._first_keyword()
-    if first_keyword and not contents.first_keyword == first_keyword:
+    expected_first_keyword = self._first_keyword()
+    if expected_first_keyword and not first_keyword == expected_first_keyword:
       raise ValueError("Descriptor must start with a '%s' entry" % first_keyword)
     
-    last_keyword = self._last_keyword()
-    if last_keyword and not contents.last_keyword == self._last_keyword():
+    expected_last_keyword = self._last_keyword()
+    if expected_last_keyword and not last_keyword == expected_last_keyword:
       raise ValueError("Descriptor must end with a '%s' entry" % last_keyword)
     
     if not self.exit_policy:
@@ -581,23 +590,28 @@ class BridgeDescriptorV3(ServerDescriptorV3):
   
   def _parse(self, entries, validate):
     ServerDescriptorV3._parse(self, entries, validate)
+    if validate: self._check_scrubbing()
+  
+  def _check_scrubbing(self):
+    """
+    Checks that our attributes have been scrubbed in accordance with the bridge
+    descriptor specification.
+    """
     
-    if validate:
-      # checks that we properly scrubbed fields
-      if self.nickname != "Unnamed":
-        raise ValueError("Router line's nickname should be scrubbed to be 'Unnamed': %s" % self.nickname)
-      elif not self.address.startswith("10."):
-        raise ValueError("Router line's address should be scrubbed to be '10.x.x.x': %s" % self.address)
-      elif self.contact and self.contact != "somebody":
-        raise ValueError("Contact line should be scrubbed to be 'somebody', but instead had '%s'" % self.contact)
-      
-      for line in self.get_unrecognized_lines():
-        if line.startswith("onion-key "):
-          raise ValueError("Bridge descriptors should have their onion-key scrubbed: %s" % self.onion_key)
-        elif line.startswith("signing-key "):
-          raise ValueError("Bridge descriptors should have their signing-key scrubbed: %s" % self.signing_key)
-        elif line.startswith("router-signature "):
-          raise ValueError("Bridge descriptors should have their signature scrubbed: %s" % self.signature)
+    if self.nickname != "Unnamed":
+      raise ValueError("Router line's nickname should be scrubbed to be 'Unnamed': %s" % self.nickname)
+    elif not self.address.startswith("10."):
+      raise ValueError("Router line's address should be scrubbed to be '10.x.x.x': %s" % self.address)
+    elif self.contact and self.contact != "somebody":
+      raise ValueError("Contact line should be scrubbed to be 'somebody', but instead had '%s'" % self.contact)
+    
+    for line in self.get_unrecognized_lines():
+      if line.startswith("onion-key "):
+        raise ValueError("Bridge descriptors should have their onion-key scrubbed: %s" % line)
+      elif line.startswith("signing-key "):
+        raise ValueError("Bridge descriptors should have their signing-key scrubbed: %s" % line)
+      elif line.startswith("router-signature "):
+        raise ValueError("Bridge descriptors should have their signature scrubbed: %s" % line)
   
   def _required_fields(self):
     # bridge required fields are the same as a relay descriptor, minus items
@@ -617,10 +631,9 @@ class BridgeDescriptorV3(ServerDescriptorV3):
   def _first_keyword(self):
     return "router"
 
-class _DescriptorContents:
+def _get_descriptor_components(raw_contents, validate):
   """
-  Initial breakup of the server descriptor contents to make parsing easier
-  later.
+  Initial breakup of the server descriptor contents to make parsing easier.
   
   A descriptor contains a series of 'keyword lines' which are simply a keyword
   followed by an optional value. Lines can also be followed by a signature
@@ -630,55 +643,62 @@ class _DescriptorContents:
   influences the resulting exit policy, but for everything else the order does
   not matter so breaking it into key / value pairs.
   
-  Attributes:
-    entries (dict)      - keyword => (value, pgp key) entries
-    first_keyword (str) - keyword of the first line
-    last_keyword (str)  - keyword of the last line
-    exit_policy (list)  - lines containing the exit policy
+  Arguments:
+    raw_contents (str) - descriptor content provided by the relay
+    validate (bool)    - checks the validity of the descriptor's content if
+                         True, skips these checks otherwise
+  
+  Returns:
+    tuple with the following attributes...
+      entries (dict)      - keyword => (value, pgp key) entries
+      first_keyword (str) - keyword of the first line
+      last_keyword (str)  - keyword of the last line
+      exit_policy (list)  - lines containing the exit policy
   """
   
-  def __init__(self, raw_contents, validate):
-    self.entries = {}
-    self.first_keyword = None
-    self.last_keyword = None
-    self.exit_policy = []
-    remaining_lines = raw_contents.split("\n")
+  entries = {}
+  first_keyword = None
+  last_keyword = None
+  exit_policy = []
+  remaining_lines = raw_contents.split("\n")
+  
+  while remaining_lines:
+    line = remaining_lines.pop(0)
     
-    while remaining_lines:
-      line = remaining_lines.pop(0)
-      
-      # last line can be empty
-      if not line and not remaining_lines: continue
-      
-      # Some lines have an 'opt ' for backward compatability. They should be
-      # ignored. This prefix is being removed in...
-      # https://trac.torproject.org/projects/tor/ticket/5124
-      
-      if line.startswith("opt "): line = line[4:]
-      
-      line_match = KEYWORD_LINE.match(line)
-      
-      if not line_match:
-        if not validate: continue
-        raise ValueError("Line contains invalid characters: %s" % line)
-      
-      keyword, value = line_match.groups()
-      
-      if not self.first_keyword: self.first_keyword = keyword
-      self.last_keyword = keyword
-      
-      try:
-        block_contents = _get_pseudo_pgp_block(remaining_lines)
-      except ValueError, exc:
-        if not validate: continue
-        raise exc
-      
-      if keyword in ("accept", "reject"):
-        self.exit_policy.append("%s %s" % (keyword, value))
-      elif keyword in self.entries:
-        self.entries[keyword].append((value, block_contents))
-      else:
-        self.entries[keyword] = [(value, block_contents)]
+    # last line can be empty
+    if not line and not remaining_lines: continue
+    
+    # Some lines have an 'opt ' for backward compatability. They should be
+    # ignored. This prefix is being removed in...
+    # https://trac.torproject.org/projects/tor/ticket/5124
+    
+    if line.startswith("opt "): line = line[4:]
+    
+    line_match = KEYWORD_LINE.match(line)
+    
+    if not line_match:
+      if not validate: continue
+      raise ValueError("Line contains invalid characters: %s" % line)
+    
+    keyword, value = line_match.groups()
+    
+    if not first_keyword: first_keyword = keyword
+    last_keyword = keyword
+    
+    try:
+      block_contents = _get_pseudo_pgp_block(remaining_lines)
+    except ValueError, exc:
+      if not validate: continue
+      raise exc
+    
+    if keyword in ("accept", "reject"):
+      exit_policy.append("%s %s" % (keyword, value))
+    elif keyword in entries:
+      entries[keyword].append((value, block_contents))
+    else:
+      entries[keyword] = [(value, block_contents)]
+  
+  return entries, first_keyword, last_keyword, exit_policy
 
 def _get_pseudo_pgp_block(remaining_contents):
   """





More information about the tor-commits mailing list