[tor-commits] [stem/master] Support for ADDRMAP events

atagar at torproject.org atagar at torproject.org
Mon Dec 3 02:35:44 UTC 2012


commit ab6e7a365cfa1e31b269f1df0e722ea11a2c53b1
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Nov 19 00:09:09 2012 -0800

    Support for ADDRMAP events
    
    There's a special spot in hell for whoever decided to allow for quoted values
    in events. This implements and adds testing for ADDRMAP events. Unlike TorCtl
    we aren't falling back on a regex for the... er, 'wonderful' quoted stuff, but
    rather including quoted value support in the Event parser.
    
    Got test data by visiting a few sites in TBB...
    
    650 ADDRMAP check.torproject.org 38.229.72.22 "2012-11-18 22:48:34" EXPIRES="2012-11-19 06:48:34"
    650 ADDRMAP ocsp.digicert.com 5.63.145.124 "2012-11-18 21:53:42" EXPIRES="2012-11-19 05:53:42"
    650 ADDRMAP www.atagar.com 75.119.206.243 "2012-11-19 00:50:13" EXPIRES="2012-11-19 08:50:13"
---
 docs/api/response.rst        |    1 +
 stem/response/events.py      |   89 ++++++++++++++++++++++++++++++++++++++----
 test/unit/response/events.py |   17 ++++++++
 3 files changed, 99 insertions(+), 8 deletions(-)

diff --git a/docs/api/response.rst b/docs/api/response.rst
index 2f71256..a73f172 100644
--- a/docs/api/response.rst
+++ b/docs/api/response.rst
@@ -17,6 +17,7 @@ Events
 
 .. autoclass:: stem.response.events.Event
 .. autoclass:: stem.response.events.LogEvent
+.. autoclass:: stem.response.events.AddrMapEvent
 .. autoclass:: stem.response.events.BandwidthEvent
 .. autoclass:: stem.response.events.CircuitEvent
 .. autoclass:: stem.response.events.NewDescEvent
diff --git a/stem/response/events.py b/stem/response/events.py
index 3df5430..941e796 100644
--- a/stem/response/events.py
+++ b/stem/response/events.py
@@ -1,4 +1,5 @@
 import re
+import datetime
 
 import stem
 import stem.control
@@ -30,14 +31,13 @@ class Event(stem.response.ControlMessage):
   
   _POSITIONAL_ARGS = ()
   _KEYWORD_ARGS = {}
+  _QUOTED = ()
   
   def _parse_message(self, arrived_at):
-    fields = str(self).split()
-    
-    if not fields:
+    if not str(self).strip():
       raise stem.ProtocolError("Received a blank tor event. Events must at the very least have a type.")
     
-    self.type = fields.pop(0)
+    self.type = str(self).split().pop(0)
     self.arrived_at = arrived_at
     
     # if we're a recognized event type then translate ourselves into that subclass
@@ -45,13 +45,32 @@ class Event(stem.response.ControlMessage):
     if self.type in EVENT_TYPE_TO_CLASS:
       self.__class__ = EVENT_TYPE_TO_CLASS[self.type]
     
+    self.positional_args = []
+    self.keyword_args = {}
+    
+    # Whoever decided to allow for quoted attributes in events should be
+    # punished. Preferably under some of those maritime laws that allow for
+    # flogging. Event parsing was nice until we threw this crap in...
+    #
+    # Pulling quoted keyword arguments out here. Quoted positonal arguments
+    # are handled later.
+    
+    content = str(self)
+    
+    for keyword in set(self._QUOTED).intersection(set(self._KEYWORD_ARGS.keys())):
+      match = re.match("^(.*) %s=\"(.*)\"(.*)$" % keyword, content)
+      
+      if match:
+        prefix, value, suffix = match.groups()
+        content = prefix + suffix
+        self.keyword_args[keyword] = value
+    
+    fields = content.split()[1:]
+    
     # Tor events contain some number of positional arguments followed by
     # key/value mappings. Parsing keyword arguments from the end until we hit
     # something that isn't a key/value mapping. The rest are positional.
     
-    self.positional_args = []
-    self.keyword_args = {}
-    
     while fields:
       kw_match = KW_ARG.match(fields[-1])
       
@@ -69,7 +88,25 @@ class Event(stem.response.ControlMessage):
     
     for i in xrange(len(self._POSITIONAL_ARGS)):
       attr_name = self._POSITIONAL_ARGS[i]
-      attr_value = self.positional_args[i] if i < len(self.positional_args) else None
+      attr_value = None
+      
+      if self.positional_args:
+        if attr_name in self._QUOTED:
+          attr_values = [self.positional_args.pop(0)]
+          
+          if not attr_values[0].startswith('"'):
+            raise stem.ProtocolError("The %s value should be quoted, but didn't have a starting quote: %s" % self)
+          
+          while True:
+            if not self.positional_args:
+              raise stem.ProtocolError("The %s value should be quoted, but didn't have an ending quote: %s" % self)
+            
+            attr_values.append(self.positional_args.pop(0))
+            if attr_values[-1].endswith('"'): break
+          
+          attr_value = " ".join(attr_values)[1:-1]
+        else:
+          attr_value = self.positional_args.pop(0)
       
       setattr(self, attr_name, attr_value)
     
@@ -82,6 +119,41 @@ class Event(stem.response.ControlMessage):
   def _parse(self):
     pass
 
+class AddrMapEvent(Event):
+  """
+  Event that indicates a new address mapping.
+  
+  :var str hostname: address being resolved
+  :var str destination: destionation of the resolution, this is usually an ip,
+    but could be a hostname if TrackHostExits is enabled or **NONE** if the
+    resolution failed
+  :var datetime expiry: expiration time of the resolution in local time
+  :var str error: error code if the resolution failed
+  :var datetime gmt_expiry: expiration time of the resolution in gmt
+  """
+  
+  # TODO: The spec for this event is a little vague. Making a couple guesses
+  # about it...
+  #
+  # https://trac.torproject.org/7515
+  
+  _POSITIONAL_ARGS = ("hostname", "destination", "expiry")
+  _KEYWORD_ARGS = {
+    "error": "error",
+    "EXPIRES": "gmt_expiry",
+  }
+  _QUOTED = ("expiry", "EXPIRES")
+  
+  def _parse(self):
+    if self.destination == "<error>":
+      self.destination = None
+    
+    if self.expiry != None:
+      self.expiry = datetime.datetime.strptime(self.expiry, "%Y-%m-%d %H:%M:%S")
+    
+    if self.gmt_expiry != None:
+      self.gmt_expiry = datetime.datetime.strptime(self.gmt_expiry, "%Y-%m-%d %H:%M:%S")
+
 class BandwidthEvent(Event):
   """
   Event emitted every second with the bytes sent and received by tor.
@@ -367,6 +439,7 @@ EVENT_TYPE_TO_CLASS = {
   "NOTICE": LogEvent,
   "WARN": LogEvent,
   "ERR": LogEvent,
+  "ADDRMAP": AddrMapEvent,
   "BW": BandwidthEvent,
   "CIRC": CircuitEvent,
   "NEWDESC": NewDescEvent,
diff --git a/test/unit/response/events.py b/test/unit/response/events.py
index 733d826..c5eeb5b 100644
--- a/test/unit/response/events.py
+++ b/test/unit/response/events.py
@@ -66,6 +66,12 @@ NEWDESC_SINGLE = "650 NEWDESC $B3FA3110CC6F42443F039220C134CBD2FC4F0493=Sakura"
 NEWDESC_MULTIPLE = "650 NEWDESC $BE938957B2CA5F804B3AFC2C1EE6673170CDBBF8=Moonshine \
 $B4BE08B22D4D2923EDC3970FD1B93D0448C6D8FF~Unnamed"
 
+# ADDRMAP event
+# TODO: it would be nice to have an example of an error event
+
+ADDRMAP = '650 ADDRMAP www.atagar.com 75.119.206.243 "2012-11-19 00:50:13" \
+EXPIRES="2012-11-19 08:50:13"'
+
 def _get_event(content):
   controller_event = mocking.get_message(content)
   stem.response.convert("EVENT", controller_event, arrived_at = 25)
@@ -121,6 +127,17 @@ class TestEvents(unittest.TestCase):
     self.assertEqual("WARN", event.runlevel)
     self.assertEqual("a multi-line\nwarning message", event.message)
   
+  def test_addrmap_event(self):
+    event = _get_event(ADDRMAP)
+    
+    self.assertTrue(isinstance(event, stem.response.events.AddrMapEvent))
+    self.assertEqual(ADDRMAP.lstrip("650 "), str(event))
+    self.assertEqual("www.atagar.com", event.hostname)
+    self.assertEqual("75.119.206.243", event.destination)
+    self.assertEqual(datetime.datetime(2012, 11, 19, 0, 50, 13), event.expiry)
+    self.assertEqual(None, event.error)
+    self.assertEqual(datetime.datetime(2012, 11, 19, 8, 50, 13), event.gmt_expiry)
+  
   def test_bw_event(self):
     event = _get_event("650 BW 15 25")
     





More information about the tor-commits mailing list