[stem/master] Support for ADDRMAP events

commit ab6e7a365cfa1e31b269f1df0e722ea11a2c53b1 Author: Damian Johnson <atagar@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")
participants (1)
-
atagar@torproject.org