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

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


commit a1a5784d480421af4394b3e012d6f4bbb8ee6c8b
Author: Damian Johnson <atagar at 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)
     





More information about the tor-commits mailing list