[stem/master] Support for ORCONN events

commit a1a5784d480421af4394b3e012d6f4bbb8ee6c8b Author: Damian Johnson <atagar@torproject.org> Date: Sun Nov 18 14:44:05 2012 -0800 Support for ORCONN events Implementation and tests for ORCONN events. These have several holes in its documentation (https://trac.torproject.org/7513) so I'm not really sure what these events actually are. Reguardless, got some samples by connecting to TBB and issuing a NEWNYM. AUTHENTICATE 250 OK SETEVENTS ORCONN 250 OK 650 ORCONN $1D024F41EDBF3F061E1341D516543090D8A44B42=AccessNowKromyon21 CONNECTED 650 ORCONN $7ED90E2833EE38A75795BA9237B0A4560E51E1A0=GreenDragon CONNECTED 650 ORCONN $A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1~tama CLOSED REASON=DONE --- stem/__init__.py | 82 ++++++++++++++++++++++++++++++++++++---- stem/control.py | 70 ++++++++++++++++++++-------------- stem/response/events.py | 86 ++++++++++++++++++++++++++++++++++++++--- test/unit/response/events.py | 32 +++++++++++++++ 4 files changed, 227 insertions(+), 43 deletions(-) diff --git a/stem/__init__.py b/stem/__init__.py index 7b3f6ee..f0fca41 100644 --- a/stem/__init__.py +++ b/stem/__init__.py @@ -62,7 +62,7 @@ Library for working with the tor process. .. data:: CircClosureReason (enum) Reason that a circuit is being closed or failed to be established. Tor may - provide purposes not in this enum. + provide reasons not in this enum. ========================= =========== CircClosureReason Description @@ -138,7 +138,7 @@ Library for working with the tor process. .. data:: StreamClosureReason (enum) Reason that a stream is being closed or failed to be established. Tor may - provide purposes not in this enum. + provide reasons not in this enum. ===================== =========== StreamClosureReason Description @@ -163,7 +163,8 @@ Library for working with the tor process. .. data:: StreamSource (enum) - Cause of a stream being remapped to another address. + Cause of a stream being remapped to another address. Tor may provide sources + not in this enum. ============= =========== StreamSource Description @@ -177,15 +178,58 @@ Library for working with the tor process. Purpsoe of the stream. This is only provided with new streams and tor may provide purposes not in this enum. + Enum descriptions are pending... + https://trac.torproject.org/7508 + ================= =========== StreamPurpose Description ================= =========== - **DIR_FETCH** unknown (https://trac.torproject.org/7508) - **UPLOAD_DESC** unknown (https://trac.torproject.org/7508) - **DNS_REQUEST** unknown (https://trac.torproject.org/7508) - **USER** unknown (https://trac.torproject.org/7508) - **DIRPORT_TEST** unknown (https://trac.torproject.org/7508) + **DIR_FETCH** unknown + **UPLOAD_DESC** unknown + **DNS_REQUEST** unknown + **USER** unknown + **DIRPORT_TEST** unknown ================= =========== + +.. data:: ORStatus (enum) + + State that an OR connection can have. Tor may provide states not in this + enum. + + Enum descriptions are pending... + https://trac.torproject.org/7513 + + =============== =========== + ORStatus Description + =============== =========== + **NEW** unknown + **LAUNCHED** unknown + **CONNECTED** unknown + **FAILED** unknown + **CLOSED** unknown + =============== =========== + +.. data:: ORClosureReason (enum) + + Reason that an OR connection is being closed or failed to be established. Tor + may provide reasons not in this enum. + + Enum descriptions are pending... + https://trac.torproject.org/7513 + + =================== =========== + ORClosureReason Description + =================== =========== + **MISC** unknown + **DONE** unknown + **CONNECTREFUSED** unknown + **IDENTITY** unknown + **CONNECTRESET** unknown + **TIMEOUT** unknown + **NOROUTE** unknown + **IOERROR** unknown + **RESOURCELIMIT** unknown + =================== =========== """ __version__ = '0.0.1' @@ -222,6 +266,8 @@ __all__ = [ "StreamClosureReason", "StreamSource", "StreamPurpose", + "ORStatus", + "ORClosureReason", ] import stem.util.enum @@ -377,3 +423,23 @@ StreamPurpose = stem.util.enum.UppercaseEnum( "DIRPORT_TEST", ) +ORStatus = stem.util.enum.UppercaseEnum( + "NEW", + "LAUNCHED", + "CONNECTED", + "FAILED", + "CLOSED", +) + +ORClosureReason = stem.util.enum.UppercaseEnum( + "MISC", + "DONE", + "CONNECTREFUSED", + "IDENTITY", + "CONNECTRESET", + "TIMEOUT", + "NOROUTE", + "IOERROR", + "RESOURCELIMIT", +) + diff --git a/stem/control.py b/stem/control.py index d7dec9a..4a91129 100644 --- a/stem/control.py +++ b/stem/control.py @@ -1500,36 +1500,50 @@ def _parse_circ_path(path): :raises: :class:`stem.ProtocolError` if the path is malformed """ - if not path: return [] - - circ_path = [] - - for path_component in path.split(','): - if '=' in path_component: - # common case - fingerprint, nickname = path_component.split('=') - elif '~' in path_component: - # this is allowed for by the spec, but I've never seen it used - fingerprint, nickname = path_component.split('~') - elif path_component[0] == '$': - # old style, fingerprint only - fingerprint, nickname = path_component, None - else: - # old style, nickname only - fingerprint, nickname = None, path_component - - if fingerprint != None: - if not stem.util.tor_tools.is_valid_fingerprint(fingerprint, True): - raise stem.ProtocolError("Fingerprint in the circuit path is malformed (%s): %s" % (fingerprint, path)) - - fingerprint = fingerprint[1:] # strip off the leading '$' - - if nickname != None and not stem.util.tor_tools.is_valid_nickname(nickname): - raise stem.ProtocolError("Nickname in the circuit path is malformed (%s): %s" % (fingerprint, path)) + if path: + try: + return [_parse_circ_entry(entry) for entry in path.split(',')] + except stem.ProtocolError, exc: + # include the path with the exception + raise stem.ProtocolError("%s: %s" % (exc, path)) + else: + return [] + +def _parse_circ_entry(entry): + """ + Parses a single relay's 'LongName' or 'ServerID'. See the + :func:`~_stem.control._parse_circ_path` function for more information. + + :param str entry: relay information to be parsed + + :returns: **(fingerprint, nickname)** tuple + + :raises: :class:`stem.ProtocolError` if the entry is malformed + """ + + if '=' in entry: + # common case + fingerprint, nickname = entry.split('=') + elif '~' in entry: + # this is allowed for by the spec, but I've never seen it used + fingerprint, nickname = entry.split('~') + elif entry[0] == '$': + # old style, fingerprint only + fingerprint, nickname = entry, None + else: + # old style, nickname only + fingerprint, nickname = None, entry + + if fingerprint != None: + if not stem.util.tor_tools.is_valid_fingerprint(fingerprint, True): + raise stem.ProtocolError("Fingerprint in the circuit path is malformed (%s)" % fingerprint) - circ_path.append((fingerprint, nickname)) + fingerprint = fingerprint[1:] # strip off the leading '$' + + if nickname != None and not stem.util.tor_tools.is_valid_nickname(nickname): + raise stem.ProtocolError("Nickname in the circuit path is malformed (%s)" % fingerprint) - return circ_path + return (fingerprint, nickname) def _case_insensitive_lookup(entries, key, default = UNDEFINED): """ diff --git a/stem/response/events.py b/stem/response/events.py index 0e7a71e..6a98ef6 100644 --- a/stem/response/events.py +++ b/stem/response/events.py @@ -12,6 +12,10 @@ from stem.util import connection, log, str_tools, tor_tools KW_ARG = re.compile("([A-Za-z0-9_]+)=(.*)") +# base message for when we get attributes not covered by our enums + +UNRECOGNIZED_ATTR_MSG = "%s event had an unrecognized %%s (%%s). Maybe a new addition to the control protocol? Full Event: '%s'" + class Event(stem.response.ControlMessage): """ Base for events we receive asynchronously, as described in section 4.1 of the @@ -150,7 +154,7 @@ class CircuitEvent(Event): # log if we have an unrecognized status, build flag, purpose, hidden # service state, or closure reason - unrecognized_msg = "CIRC event had an unrecognized %%s (%%s). Maybe a new addition to the control protocol? Full Event: '%s'" % self + unrecognized_msg = UNRECOGNIZED_ATTR_MSG % ("CIRC", self) if self.status and (not self.status in stem.CircStatus): log_id = "event.circ.unknown_status.%s" % self.status @@ -195,6 +199,73 @@ class LogEvent(Event): self.message = str(self)[len(self.runlevel) + 1:].rstrip("\nOK") +class ORConnEvent(Event): + """ + Event that indicates a change in a relay connection. The 'endpoint' could be + any of several things including a... + + * fingerprint + * nickname + * 'fingerprint=nickname' pair + * address:port + + The derived 'endpoint_*' attributes are generally more useful. + + :var str endpoint: relay that the event concerns + :var str endpoint_fingerprint: endpoint's finterprint if it was provided + :var str endpoint_nickname: endpoint's nickname if it was provided + :var str endpoint_address: endpoint's address if it was provided + :var int endpoint_port: endpoint's port if it was provided + :var stem.ORStatus status: state of the connection + :var stem.ORClosureReason reason: reason for the connection to be closed + :var int circ_count: number of established and pending circuits + """ + + _POSITIONAL_ARGS = ("endpoint", "status") + _KEYWORD_ARGS = { + "REASON": "reason", + "NCIRCS": "circ_count", + } + + def _parse(self): + self.endpoint_fingerprint = None + self.endpoint_nickname = None + self.endpoint_address = None + self.endpoint_port = None + + try: + self.endpoint_fingerprint, self.endpoint_nickname = \ + stem.control._parse_circ_entry(self.endpoint) + except stem.ProtocolError: + if not ':' in self.endpoint: + raise stem.ProtocolError("ORCONN endpoint is neither a relay nor 'address:port': %s" % self) + + address, port = self.endpoint.split(':', 1) + + if not connection.is_valid_port(port): + raise stem.ProtocolError("ORCONN's endpoint location's port is invalid: %s" % self) + + self.endpoint_address = address + self.endpoint_port = int(port) + + if self.circ_count != None: + if not self.circ_count.isdigit(): + raise stem.ProtocolError("ORCONN event got a non-numeric circuit count (%s): %s" % (self.circ_count, self)) + + self.circ_count = int(self.circ_count) + + # log if we have an unrecognized status or reason + + unrecognized_msg = UNRECOGNIZED_ATTR_MSG % ("ORCONN", self) + + if self.status and (not self.status in stem.ORStatus): + log_id = "event.orconn.unknown_status.%s" % self.status + log.log_once(log_id, log.INFO, unrecognized_msg % ('status', self.status)) + + if self.reason and (not self.reason in stem.ORClosureReason): + log_id = "event.orconn.unknown_reason.%s" % self.reason + log.log_once(log_id, log.INFO, unrecognized_msg % ('reason', self.reason)) + class StreamEvent(Event): """ Event that indicates that a stream has changed. @@ -231,7 +302,7 @@ class StreamEvent(Event): if not ':' in self.target: raise stem.ProtocolError("Target location must be of the form 'address:port': %s" % self) - address, port = self.target.split(':') + address, port = self.target.split(':', 1) if not connection.is_valid_port(port): raise stem.ProtocolError("Target location's port is invalid: %s" % self) @@ -246,7 +317,7 @@ class StreamEvent(Event): if not ':' in self.source_addr: raise stem.ProtocolError("Source location must be of the form 'address:port': %s" % self) - address, port = self.source_addr.split(':') + address, port = self.source_addr.split(':', 1) if not connection.is_valid_port(port): raise stem.ProtocolError("Source location's port is invalid: %s" % self) @@ -261,7 +332,7 @@ class StreamEvent(Event): # log if we have an unrecognized closure reason or purpose - unrecognized_msg = "STREAM event had an unrecognized %%s (%%s). Maybe a new addition to the control protocol? Full Event: '%s'" % self + unrecognized_msg = UNRECOGNIZED_ATTR_MSG % ("STREAM", self) if self.reason and (not self.reason in stem.StreamClosureReason): log_id = "event.stream.reason.%s" % self.reason @@ -276,13 +347,14 @@ class StreamEvent(Event): log.log_once(log_id, log.INFO, unrecognized_msg % ('purpose', self.purpose)) EVENT_TYPE_TO_CLASS = { - "CIRC": CircuitEvent, - "STREAM": StreamEvent, - "BW": BandwidthEvent, "DEBUG": LogEvent, "INFO": LogEvent, "NOTICE": LogEvent, "WARN": LogEvent, "ERR": LogEvent, + "BW": BandwidthEvent, + "CIRC": CircuitEvent, + "ORCONN": ORConnEvent, + "STREAM": StreamEvent, } diff --git a/test/unit/response/events.py b/test/unit/response/events.py index 3fe22de..c17e84d 100644 --- a/test/unit/response/events.py +++ b/test/unit/response/events.py @@ -54,6 +54,11 @@ STREAM_SUCCEEDED = "650 STREAM 18 SUCCEEDED 26 74.125.227.129:443" STREAM_CLOSED_RESET = "650 STREAM 21 CLOSED 26 74.125.227.129:443 REASON=CONNRESET" STREAM_CLOSED_DONE = "650 STREAM 25 CLOSED 26 199.7.52.72:80 REASON=DONE" +# ORCONN events from starting tor 0.2.2.39 via TBB + +ORCONN_CONNECTED = "650 ORCONN $7ED90E2833EE38A75795BA9237B0A4560E51E1A0=GreenDragon CONNECTED" +ORCONN_CLOSED = "650 ORCONN $A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1~tama CLOSED REASON=DONE" + def _get_event(content): controller_event = mocking.get_message(content) stem.response.convert("EVENT", controller_event, arrived_at = 25) @@ -181,6 +186,33 @@ class TestEvents(unittest.TestCase): self.assertEqual(None, event.reason) self.assertEqual(None, event.remote_reason) + def test_orconn_event(self): + event = _get_event(ORCONN_CONNECTED) + + self.assertTrue(isinstance(event, stem.response.events.ORConnEvent)) + self.assertEqual(ORCONN_CONNECTED.lstrip("650 "), str(event)) + self.assertEqual("$7ED90E2833EE38A75795BA9237B0A4560E51E1A0=GreenDragon", event.endpoint) + self.assertEqual("7ED90E2833EE38A75795BA9237B0A4560E51E1A0", event.endpoint_fingerprint) + self.assertEqual("GreenDragon", event.endpoint_nickname) + self.assertEqual(None, event.endpoint_address) + self.assertEqual(None, event.endpoint_port) + self.assertEqual(ORStatus.CONNECTED, event.status) + self.assertEqual(None, event.reason) + self.assertEqual(None, event.circ_count) + + event = _get_event(ORCONN_CLOSED) + + self.assertTrue(isinstance(event, stem.response.events.ORConnEvent)) + self.assertEqual(ORCONN_CLOSED.lstrip("650 "), str(event)) + self.assertEqual("$A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1~tama", event.endpoint) + self.assertEqual("A1130635A0CDA6F60C276FBF6994EFBD4ECADAB1", event.endpoint_fingerprint) + self.assertEqual("tama", event.endpoint_nickname) + self.assertEqual(None, event.endpoint_address) + self.assertEqual(None, event.endpoint_port) + self.assertEqual(ORStatus.CLOSED, event.status) + self.assertEqual(ORClosureReason.DONE, event.reason) + self.assertEqual(None, event.circ_count) + def test_stream_event(self): event = _get_event(STREAM_NEW)
participants (1)
-
atagar@torproject.org