[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