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

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


commit a349a01fece5534195a6619d72df4944065001b8
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Nov 17 18:49:23 2012 -0800

    Support for CIRC events
    
    Implementation and testing for CIRC events. This work also concerns the
    'GETINFO circuit-status' method, which is defined as providing the same output
    as CIRC events. This is part of the reason why I put the enums for the event
    attributes in 'stem.control'.
---
 docs/api/response.rst           |    1 +
 run_tests.py                    |    2 +
 stem/control.py                 |  227 +++++++++++++++++++++++++++++++++++++++
 stem/response/events.py         |   92 +++++++++++++++-
 stem/util/str_tools.py          |   36 ++++++
 stem/util/tor_tools.py          |   18 +++
 test/unit/__init__.py           |    1 +
 test/unit/control/__init__.py   |    6 +
 test/unit/control/controller.py |   50 +++++++++
 test/unit/response/events.py    |  124 +++++++++++++++++++++-
 test/unit/util/str_tools.py     |   31 ++++++
 11 files changed, 582 insertions(+), 6 deletions(-)

diff --git a/docs/api/response.rst b/docs/api/response.rst
index fd6e2cc..043cae4 100644
--- a/docs/api/response.rst
+++ b/docs/api/response.rst
@@ -18,4 +18,5 @@ Events
 .. autoclass:: stem.response.events.Event
 .. autoclass:: stem.response.events.LogEvent
 .. autoclass:: stem.response.events.BandwidthEvent
+.. autoclass:: stem.response.events.CircuitEvent
 
diff --git a/run_tests.py b/run_tests.py
index f490da7..a9aebbe 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -16,6 +16,7 @@ import test.output
 import test.runner
 import test.check_whitespace
 import test.unit.connection.authentication
+import test.unit.control.controller
 import test.unit.descriptor.export
 import test.unit.descriptor.reader
 import test.unit.descriptor.server_descriptor
@@ -142,6 +143,7 @@ UNIT_TESTS = (
   test.unit.response.protocolinfo.TestProtocolInfoResponse,
   test.unit.response.authchallenge.TestAuthChallengeResponse,
   test.unit.connection.authentication.TestAuthenticate,
+  test.unit.control.controller.TestControl,
 )
 
 INTEG_TESTS = (
diff --git a/stem/control.py b/stem/control.py
index 1c1139f..083082e 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -78,7 +78,110 @@ providing its own for interacting at a higher level.
   **WARN**    :class:`stem.response.events.LogEvent`
   **ERR**     :class:`stem.response.events.LogEvent`
   **BW**      :class:`stem.response.events.BandwidthEvent`
+  **CIRC**    :class:`stem.response.events.CircuitEvent`
   =========== ===========
+
+.. data:: CircStatus (enum)
+  
+  Statuses that a circuit can be in. Tor may provide statuses not in this enum.
+  
+  ============ ===========
+  CircStatus   Description
+  ============ ===========
+  **LAUNCHED** new circuit was created
+  **BUILT**    circuit finished being created and can accept traffic
+  **EXTENDED** circuit has been extended by a hop
+  **FAILED**   circuit construction failed
+  **CLOSED**   circuit has been closed
+  ============ ===========
+
+.. data:: CircBuildFlag (enum)
+  
+  Attributes about how a circuit is built. These were introduced in tor version
+  0.2.3.11. Tor may provide flags not in this enum.
+  
+  ================= ===========
+  CircBuildFlag     Description
+  ================= ===========
+  **ONEHOP_TUNNEL** single hop circuit to fetch directory information
+  **IS_INTERNAL**   circuit that won't be used for client traffic
+  **NEED_CAPACITY** circuit only includes high capacity relays
+  **NEED_UPTIME**   circuit only includes relays with a high uptime
+  ================= ===========
+
+.. data:: CircPurpose (enum)
+  
+  Description of what a circuit is intended for. These were introduced in tor
+  version 0.2.1.6. Tor may provide purposes not in this enum.
+  
+  ==================== ===========
+  CircPurpose          Description
+  ==================== ===========
+  **GENERAL**          client traffic or fetching directory information
+  **HS_CLIENT_INTRO**  client side introduction point for a hidden service circuit
+  **HS_CLIENT_REND**   client side hidden service rendezvous circuit
+  **HS_SERVICE_INTRO** server side introduction point for a hidden service circuit
+  **HS_SERVICE_REND**  server side hidden service rendezvous circuit
+  **TESTING**          testing to see if we're reachable, so we can be used as a relay
+  **CONTROLLER**       circuit that was built by a controller
+  ==================== ===========
+
+.. data:: CircClosureReason (enum)
+  
+  Reason that a circuit is being closed or failed to be established. Tor may
+  provide purposes not in this enum.
+  
+  ========================= ===========
+  CircClosureReason         Description
+  ========================= ===========
+  **NONE**                  no reason given
+  **TORPROTOCOL**           violation in the tor protocol
+  **INTERNAL**              internal error
+  **REQUESTED**             requested by the client via a TRUNCATE command
+  **HIBERNATING**           relay is presently hibernating
+  **RESOURCELIMIT**         relay is out of memory, sockets, or circuit IDs
+  **CONNECTFAILED**         unable to contact the relay
+  **OR_IDENTITY**           relay had the wrong OR identification
+  **OR_CONN_CLOSED**        connection failed after being established
+  **FINISHED**              circuit has expired (see tor's MaxCircuitDirtiness config option)
+  **TIMEOUT**               circuit construction timed out
+  **DESTROYED**             circuit unexpectedly closed
+  **NOPATH**                not enough relays to make a circuit
+  **NOSUCHSERVICE**         requested hidden service does not exist
+  **MEASUREMENT_EXPIRED**   unknown (https://trac.torproject.org/7506)
+  ========================= ===========
+
+.. data:: HiddenServiceState (enum)
+  
+  State that a hidden service circuit can have. These were introduced in tor
+  version 0.2.3.11. Tor may provide states not in this enum.
+  
+  Enumerations fall into four groups based on their prefix...
+  
+  ======= ===========
+  Prefix  Description
+  ======= ===========
+  HSCI_*  client-side introduction-point
+  HSCR_*  client-side rendezvous-point
+  HSSI_*  service-side introduction-point
+  HSSR_*  service-side rendezvous-point
+  ======= ===========
+  
+  ============================= ===========
+  HiddenServiceState            Description
+  ============================= ===========
+  **HSCI_CONNECTING**           connecting to the introductory point
+  **HSCI_INTRO_SENT**           sent INTRODUCE1 and awaiting a reply
+  **HSCI_DONE**                 received a reply, circuit is closing
+  **HSCR_CONNECTING**           connecting to the introductory point
+  **HSCR_ESTABLISHED_IDLE**     rendezvous-point established, awaiting an introduction
+  **HSCR_ESTABLISHED_WAITING**  introduction received, awaiting a rend
+  **HSCR_JOINED**               connected to the hidden service
+  **HSSI_CONNECTING**           connecting to the introductory point
+  **HSSI_ESTABLISHED**          established introductory point
+  **HSSR_CONNECTING**           connecting to the introductory point
+  **HSSR_JOINED**               connected to the rendezvous-point
+  ============================= ===========
 """
 
 from __future__ import with_statement
@@ -129,6 +232,63 @@ EventType = stem.util.enum.UppercaseEnum(
   "CIRC_MINOR",
 )
 
+CircStatus = stem.util.enum.UppercaseEnum(
+  "LAUNCHED",
+  "BUILT",
+  "EXTENDED",
+  "FAILED",
+  "CLOSED",
+)
+
+CircBuildFlag = stem.util.enum.UppercaseEnum(
+  "ONEHOP_TUNNEL",
+  "IS_INTERNAL",
+  "NEED_CAPACITY",
+  "NEED_UPTIME",
+)
+
+CircPurpose = stem.util.enum.UppercaseEnum(
+  "GENERAL",
+  "HS_CLIENT_INTRO",
+  "HS_CLIENT_REND",
+  "HS_SERVICE_INTRO",
+  "HS_SERVICE_REND",
+  "TESTING",
+  "CONTROLLER",
+)
+
+CircClosureReason = stem.util.enum.UppercaseEnum(
+  "NONE",
+  "TORPROTOCOL",
+  "INTERNAL",
+  "REQUESTED",
+  "HIBERNATING",
+  "RESOURCELIMIT",
+  "CONNECTFAILED",
+  "OR_IDENTITY",
+  "OR_CONN_CLOSED",
+  "FINISHED",
+  "TIMEOUT",
+  "DESTROYED",
+  "NOPATH",
+  "NOSUCHSERVICE",
+  "MEASUREMENT_EXPIRED",
+)
+
+HiddenServiceState = stem.util.enum.UppercaseEnum(
+  "HSCI_CONNECTING",
+  "HSCI_INTRO_SENT",
+  "HSCI_DONE",
+  "HSCR_CONNECTING",
+  "HSCR_ESTABLISHED_IDLE",
+  "HSCR_ESTABLISHED_WAITING",
+  "HSCR_JOINED",
+  "HSSI_CONNECTING",
+  "HSSI_ESTABLISHED",
+  "HSSR_CONNECTING",
+  "HSSR_JOINED",
+)
+
 # Constant to indicate an undefined argument default. Usually we'd use None for
 # this, but users will commonly provide None as the argument so need something
 # else fairly unique...
@@ -521,6 +681,9 @@ class Controller(BaseController):
   BaseController and provides a more user friendly API for library users.
   """
   
+  # TODO: We need a set_up() (and maybe tear_down()?) method, so we can
+  # reattach listeners and set VERBOSE_NAMES.
+  
   def from_port(control_addr = "127.0.0.1", control_port = 9051):
     """
     Constructs a :class:`~stem.socket.ControlPort` based Controller.
@@ -1462,6 +1625,70 @@ class Controller(BaseController):
           for listener in event_listeners:
             listener(event_message)
 
+def _parse_circ_path(path):
+  """
+  Parses a circuit path as a list of **(fingerprint, nickname)** tuples. Tor
+  circuit paths are defined as being of the form...
+  
+  ::
+  
+    Path = LongName *("," LongName)
+    LongName = Fingerprint [ ( "=" / "~" ) Nickname ]
+    
+    example:
+    $999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz
+  
+  ... *unless* this is prior to tor version 0.2.2.1 with the VERBOSE_NAMES
+  feature turned off (or before version 0.1.2.2 where the feature was
+  introduced). In that case either the fingerprint or nickname in the tuple
+  will be **None**, depending on which is missing.
+  
+  ::
+  
+    Path = ServerID *("," ServerID)
+    ServerID = Nickname / Fingerprint
+    
+    example:
+    $E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14
+  
+  :param str path: circuit path to be parsed
+  
+  :returns: list of **(fingerprint, nickname)** tuples, fingerprints do not have a proceeding '$'
+  
+  :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))
+    
+    circ_path.append((fingerprint, nickname))
+  
+  return circ_path
+
 def _case_insensitive_lookup(entries, key, default = UNDEFINED):
   """
   Makes a case insensitive lookup within a list or dictionary, providing the
diff --git a/stem/response/events.py b/stem/response/events.py
index 93c3191..4c8d8bd 100644
--- a/stem/response/events.py
+++ b/stem/response/events.py
@@ -1,7 +1,9 @@
 import re
 
+import stem.control
 import stem.response
-import stem.socket
+
+from stem.util import log, str_tools, tor_tools
 
 # Matches keyword=value arguments. This can't be a simple "(.*)=(.*)" pattern
 # because some positional arguments, like circuit paths, can have an equal
@@ -28,7 +30,7 @@ class Event(stem.response.ControlMessage):
     fields = str(self).split()
     
     if not fields:
-      raise stem.socket.ProtocolError("Received a blank tor event. Events must at the very least have a type.")
+      raise stem.ProtocolError("Received a blank tor event. Events must at the very least have a type.")
     
     self.type = fields.pop(0)
     self.arrived_at = arrived_at
@@ -75,6 +77,85 @@ class Event(stem.response.ControlMessage):
   def _parse(self):
     pass
 
+class CircuitEvent(Event):
+  """
+  Event that indicates that a circuit has changed.
+  
+  The fingerprint or nickname values in our path may be **None** if the
+  VERBOSE_NAMES feature is unavailable. The option was first introduced in tor
+  version 0.1.2.2.
+  
+  :var str id: circuit identifier
+  :var stem.control.CircStatus status: reported status for the circuit
+  :var tuple path: relays involved in the circuit, these are
+    **(fingerprint, nickname)** tuples
+  :var tuple build_flags: :data:`~stem.control.CircBuildFlag` attributes
+    governing how the circuit is built
+  :var stem.control.CircPurpose purpose: purpose that the circuit is intended for
+  :var stem.control.HiddenServiceState hs_state: status if this is a hidden service circuit
+  :var str rend_query: circuit's rendezvous-point if this is hidden service related
+  :var datetime created: time when the circuit was created or cannibalized
+  :var stem.control.CircClosureReason reason: reason for the circuit to be closed
+  :var stem.control.CircClosureReason remote_reason: remote side's reason for the circuit to be closed
+  """
+  
+  _POSITIONAL_ARGS = ("id", "status", "path")
+  _KEYWORD_ARGS = {
+    "BUILD_FLAGS": "build_flags",
+    "PURPOSE": "purpose",
+    "HS_STATE": "hs_state",
+    "REND_QUERY": "rend_query",
+    "TIME_CREATED": "created",
+    "REASON": "reason",
+    "REMOTE_REASON": "remote_reason",
+  }
+  
+  def _parse(self):
+    self.path = tuple(stem.control._parse_circ_path(self.path))
+    
+    if self.build_flags != None:
+      self.build_flags = tuple(self.build_flags.split(','))
+    
+    if self.created != None:
+      try:
+        self.created = str_tools.parse_iso_timestamp(self.created)
+      except ValueError, exc:
+        raise stem.ProtocolError("Unable to parse create date (%s): %s" % (exc, self))
+    
+    if self.id != None and not tor_tools.is_valid_circuit_id(self.id):
+      raise stem.ProtocolError("Circuit IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.id, self))
+    
+    # log if we have an unrecognized status, build flag, purpose, hidden
+    # service state, or closure reason
+    
+    unrecognized_msg = "CIRC event had an unrecognised %%s (%%s). Maybe a new addition to the control protocol? Full Event: '%s'" % self
+    
+    if self.status and (not self.status in stem.control.CircStatus):
+      log_id = "event.circ.unknown_status.%s" % self.status
+      log.log_once(log_id, log.INFO, unrecognized_msg % ('status', self.status))
+    
+    if self.build_flags:
+      for flag in self.build_flags:
+        if not flag in stem.control.CircBuildFlag:
+          log_id = "event.circ.unknown_build_flag.%s" % flag
+          log.log_once(log_id, log.INFO, unrecognized_msg % ('build flag', flag))
+    
+    if self.purpose and (not self.purpose in stem.control.CircPurpose):
+      log_id = "event.circ.unknown_purpose.%s" % self.purpose
+      log.log_once(log_id, log.INFO, unrecognized_msg % ('purpose', self.purpose))
+    
+    if self.hs_state and (not self.hs_state in stem.control.HiddenServiceState):
+      log_id = "event.circ.unknown_hs_state.%s" % self.hs_state
+      log.log_once(log_id, log.INFO, unrecognized_msg % ('hidden service state', self.hs_state))
+    
+    if self.reason and (not self.reason in stem.control.CircClosureReason):
+      log_id = "event.circ.unknown_reason.%s" % self.reason
+      log.log_once(log_id, log.INFO, unrecognized_msg % ('reason', self.reason))
+    
+    if self.remote_reason and (not self.remote_reason in stem.control.CircClosureReason):
+      log_id = "event.circ.unknown_remote_reason.%s" % self.remote_reason
+      log.log_once(log_id, log.INFO, unrecognized_msg % ('remote reason', self.remote_reason))
+
 class BandwidthEvent(Event):
   """
   Event emitted every second with the bytes sent and received by tor.
@@ -87,11 +168,11 @@ class BandwidthEvent(Event):
   
   def _parse(self):
     if not self.read:
-      raise stem.socket.ProtocolError("BW event is missing its read value")
+      raise stem.ProtocolError("BW event is missing its read value")
     elif not self.written:
-      raise stem.socket.ProtocolError("BW event is missing its written value")
+      raise stem.ProtocolError("BW event is missing its written value")
     elif not self.read.isdigit() or not self.written.isdigit():
-      raise stem.socket.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self)
+      raise stem.ProtocolError("A BW event's bytes sent and received should be a positive numeric value, received: %s" % self)
     
     self.read = long(self.read)
     self.written = long(self.written)
@@ -114,6 +195,7 @@ class LogEvent(Event):
     self.message = str(self)[len(self.runlevel) + 1:].rstrip("\nOK")
 
 EVENT_TYPE_TO_CLASS = {
+  "CIRC": CircuitEvent,
   "BW": BandwidthEvent,
   "DEBUG": LogEvent,
   "INFO": LogEvent,
diff --git a/stem/util/str_tools.py b/stem/util/str_tools.py
index f444760..6c36cd0 100644
--- a/stem/util/str_tools.py
+++ b/stem/util/str_tools.py
@@ -11,8 +11,12 @@ Toolkit for various string activity.
   get_time_labels - human readable labels for each time unit
   get_short_time_label - condensed time label output
   parse_short_time_label - seconds represented by a short time label
+  
+  parse_iso_timestamp - parses an ISO timestamp as a datetime value
 """
 
+import datetime
+
 # label conversion tuples of the form...
 # (bits / bytes / seconds, short label, long label)
 SIZE_UNITS_BITS = (
@@ -235,6 +239,38 @@ def parse_short_time_label(label):
   except ValueError:
     raise ValueError("Non-numeric value in time entry: %s" % label)
 
+def parse_iso_timestamp(entry):
+  """
+  Parses the ISO 8601 standard that provides for timestamps like...
+  
+  ::
+  
+    2012-11-08T16:48:41.420251
+  
+  :param str entry: timestamp to be parsed
+  
+  :returns: datetime for the time represented by the timestamp
+  
+  :raises: ValueError if the timestamp is malformed
+  """
+  
+  if not isinstance(entry, str):
+    raise ValueError("parse_iso_timestamp() input must be a str, got a %s" % type(entry))
+  
+  # based after suggestions from...
+  # http://stackoverflow.com/questions/127803/how-to-parse-iso-formatted-date-in-python
+  
+  if '.' in entry:
+    timestamp_str, microseconds = entry.split('.')
+  else:
+    timestamp_str, microseconds = entry, '000000'
+  
+  if len(microseconds) != 6 or not microseconds.isdigit():
+    raise ValueError("timestamp's microseconds should be six digits")
+  
+  timestamp = datetime.datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S")
+  return timestamp + datetime.timedelta(microseconds = int(microseconds))
+
 def _get_label(units, count, decimal, is_long):
   """
   Provides label corresponding to units of the highest significance in the
diff --git a/stem/util/tor_tools.py b/stem/util/tor_tools.py
index c70fbce..3673be9 100644
--- a/stem/util/tor_tools.py
+++ b/stem/util/tor_tools.py
@@ -7,16 +7,21 @@ Miscellaneous utility functions for working with tor.
 
   is_valid_fingerprint - checks if a string is a valid tor relay fingerprint
   is_valid_nickname - checks if a string is a valid tor relay nickname
+  is_valid_circuit_id - checks if a string is a valid tor circuit id
   is_hex_digits - checks if a string is only made up of hex digits
 """
 
 import re
 
 # The control-spec defines the following as...
+#
 #   Fingerprint = "$" 40*HEXDIG
 #   NicknameChar = "a"-"z" / "A"-"Z" / "0" - "9"
 #   Nickname = 1*19 NicknameChar
 #
+#   CircuitID = 1*16 IDChar
+#   IDChar = ALPHA / DIGIT
+#
 # HEXDIG is defined in RFC 5234 as being uppercase and used in RFC 5987 as
 # case insensitive. Tor doesn't define this in the spec so flipping a coin
 # and going with case insensitive.
@@ -24,6 +29,7 @@ import re
 HEX_DIGIT = "[0-9a-fA-F]"
 FINGERPRINT_PATTERN = re.compile("^%s{40}$" % HEX_DIGIT)
 NICKNAME_PATTERN = re.compile("^[a-zA-Z0-9]{1,19}$")
+CIRC_ID_PATTERN = re.compile("^[a-zA-Z0-9]{1,16}$")
 
 def is_valid_fingerprint(entry, check_prefix = False):
   """
@@ -59,6 +65,18 @@ def is_valid_nickname(entry):
   
   return bool(NICKNAME_PATTERN.match(entry))
 
+def is_valid_circuit_id(entry):
+  """
+  Checks if a string is a valid format for being a circuit identifier.
+  
+  :returns: **True** if the string could be a circuit id, **False** otherwise
+  """
+  
+  if not isinstance(entry, str):
+    return False
+  
+  return bool(CIRC_ID_PATTERN.match(entry))
+
 def is_hex_digits(entry, count):
   """
   Checks if a string is the given number of hex digits. Digits represented by
diff --git a/test/unit/__init__.py b/test/unit/__init__.py
index 15a6b88..a57679c 100644
--- a/test/unit/__init__.py
+++ b/test/unit/__init__.py
@@ -4,6 +4,7 @@ Unit tests for the stem library.
 
 __all__ = [
   "connection",
+  "control",
   "descriptor",
   "exit_policy",
   "socket",
diff --git a/test/unit/control/__init__.py b/test/unit/control/__init__.py
new file mode 100644
index 0000000..448a597
--- /dev/null
+++ b/test/unit/control/__init__.py
@@ -0,0 +1,6 @@
+"""
+Unit tests for stem.control.
+"""
+
+__all__ = ["controller"]
+
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
new file mode 100644
index 0000000..e25fbe0
--- /dev/null
+++ b/test/unit/control/controller.py
@@ -0,0 +1,50 @@
+"""
+Unit tests for the stem.control module. The module's primarily exercised via
+integ tests, but a few bits lend themselves to unit testing.
+"""
+
+import unittest
+
+from stem import ProtocolError
+from stem.control import _parse_circ_path
+
+class TestControl(unittest.TestCase):
+  def test_parse_circ_path(self):
+    """
+    Exercises the _parse_circ_path() helper function.
+    """
+    
+    # empty input
+    
+    self.assertEqual([], _parse_circ_path(None))
+    self.assertEqual([], _parse_circ_path(''))
+    
+    # check the pydoc examples
+    
+    pydoc_examples = {
+      '$999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz':
+        [('999A226EBED397F331B612FE1E4CFAE5C1F201BA', 'piyaz')],
+      '$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14':
+        [('E57A476CD4DFBD99B4EE52A100A58610AD6E80B9', None),
+         (None, 'hamburgerphone'),
+         (None, 'PrivacyRepublic14'),
+        ],
+    }
+    
+    for test_input, expected in pydoc_examples.items():
+      self.assertEqual(expected, _parse_circ_path(test_input))
+    
+    # exercise with some invalid inputs
+    
+    malformed_inputs = [
+      '=piyaz', # no fingerprint
+      '999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz', # fingerprint missing prefix
+      '$999A226EBED397F331B612FE1E4CFAE5C1F201BAA=piyaz', # fingerprint too long
+      '$999A226EBED397F331B612FE1E4CFAE5C1F201B=piyaz', # fingerprint too short
+      '$999A226EBED397F331B612FE1E4CFAE5C1F201Bz=piyaz', # invalid character in fingerprint
+      '$999A226EBED397F331B612FE1E4CFAE5C1F201BA=', # no nickname
+    ]
+    
+    for test_input in malformed_inputs:
+      self.assertRaises(ProtocolError, _parse_circ_path, test_input)
+
diff --git a/test/unit/response/events.py b/test/unit/response/events.py
index 1942b69..d0c0b4e 100644
--- a/test/unit/response/events.py
+++ b/test/unit/response/events.py
@@ -2,6 +2,7 @@
 Unit tests for the stem.response.events classes.
 """
 
+import datetime
 import threading
 import unittest
 
@@ -9,7 +10,37 @@ import stem.response
 import stem.response.events
 import test.mocking as mocking
 
-from stem.socket import ProtocolError
+from stem import ProtocolError
+from stem.control import CircStatus, CircBuildFlag, CircPurpose, CircClosureReason
+
+# CIRC events from tor v0.2.3.16
+
+CIRC_LAUNCHED = "650 CIRC 7 LAUNCHED \
+BUILD_FLAGS=NEED_CAPACITY \
+PURPOSE=GENERAL \
+TIME_CREATED=2012-11-08T16:48:38.417238"
+
+CIRC_EXTENDED = "650 CIRC 7 EXTENDED \
+$999A226EBED397F331B612FE1E4CFAE5C1F201BA=piyaz \
+BUILD_FLAGS=NEED_CAPACITY \
+PURPOSE=GENERAL \
+TIME_CREATED=2012-11-08T16:48:38.417238"
+
+CIRC_FAILED = "650 CIRC 5 FAILED \
+$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9=ergebnisoffen \
+BUILD_FLAGS=NEED_CAPACITY \
+PURPOSE=GENERAL \
+TIME_CREATED=2012-11-08T16:48:36.400959 \
+REASON=DESTROYED \
+REMOTE_REASON=OR_CONN_CLOSED"
+
+# CIRC events from tor v0.2.1.30 without the VERBOSE_NAMES feature
+
+CIRC_LAUNCHED_OLD = "650 CIRC 4 LAUNCHED"
+CIRC_EXTENDED_OLD = "650 CIRC 1 EXTENDED \
+$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone"
+CIRC_BUILT_OLD = "650 CIRC 1 BUILT \
+$E57A476CD4DFBD99B4EE52A100A58610AD6E80B9,hamburgerphone,PrivacyRepublic14"
 
 def _get_event(content):
   controller_event = mocking.get_message(content)
@@ -47,6 +78,97 @@ class TestEvents(unittest.TestCase):
     time.sleep(0.2)
     events_thread.join()
   
+  def test_circ_event(self):
+    event = _get_event(CIRC_LAUNCHED)
+    
+    self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
+    self.assertEqual(CIRC_LAUNCHED.lstrip("650 "), str(event))
+    self.assertEqual("7", event.id)
+    self.assertEqual(CircStatus.LAUNCHED, event.status)
+    self.assertEqual((), event.path)
+    self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags)
+    self.assertEqual(CircPurpose.GENERAL, event.purpose)
+    self.assertEqual(None, event.hs_state)
+    self.assertEqual(None, event.rend_query)
+    self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 38, 417238), event.created)
+    self.assertEqual(None, event.reason)
+    self.assertEqual(None, event.remote_reason)
+    
+    event = _get_event(CIRC_EXTENDED)
+    
+    self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
+    self.assertEqual(CIRC_EXTENDED.lstrip("650 "), str(event))
+    self.assertEqual("7", event.id)
+    self.assertEqual(CircStatus.EXTENDED, event.status)
+    self.assertEqual((("999A226EBED397F331B612FE1E4CFAE5C1F201BA", "piyaz"),), event.path)
+    self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags)
+    self.assertEqual(CircPurpose.GENERAL, event.purpose)
+    self.assertEqual(None, event.hs_state)
+    self.assertEqual(None, event.rend_query)
+    self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 38, 417238), event.created)
+    self.assertEqual(None, event.reason)
+    self.assertEqual(None, event.remote_reason)
+    
+    event = _get_event(CIRC_FAILED)
+    
+    self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
+    self.assertEqual(CIRC_FAILED.lstrip("650 "), str(event))
+    self.assertEqual("5", event.id)
+    self.assertEqual(CircStatus.FAILED, event.status)
+    self.assertEqual((("E57A476CD4DFBD99B4EE52A100A58610AD6E80B9", "ergebnisoffen"),), event.path)
+    self.assertEqual((CircBuildFlag.NEED_CAPACITY,), event.build_flags)
+    self.assertEqual(CircPurpose.GENERAL, event.purpose)
+    self.assertEqual(None, event.hs_state)
+    self.assertEqual(None, event.rend_query)
+    self.assertEqual(datetime.datetime(2012, 11, 8, 16, 48, 36, 400959), event.created)
+    self.assertEqual(CircClosureReason.DESTROYED, event.reason)
+    self.assertEqual(CircClosureReason.OR_CONN_CLOSED, event.remote_reason)
+    
+    event = _get_event(CIRC_LAUNCHED_OLD)
+    
+    self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
+    self.assertEqual(CIRC_LAUNCHED_OLD.lstrip("650 "), str(event))
+    self.assertEqual("4", event.id)
+    self.assertEqual(CircStatus.LAUNCHED, event.status)
+    self.assertEqual((), event.path)
+    self.assertEqual(None, event.build_flags)
+    self.assertEqual(None, event.purpose)
+    self.assertEqual(None, event.hs_state)
+    self.assertEqual(None, event.rend_query)
+    self.assertEqual(None, event.created)
+    self.assertEqual(None, event.reason)
+    self.assertEqual(None, event.remote_reason)
+    
+    event = _get_event(CIRC_EXTENDED_OLD)
+    
+    self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
+    self.assertEqual(CIRC_EXTENDED_OLD.lstrip("650 "), str(event))
+    self.assertEqual("1", event.id)
+    self.assertEqual(CircStatus.EXTENDED, event.status)
+    self.assertEqual((("E57A476CD4DFBD99B4EE52A100A58610AD6E80B9", None), (None,"hamburgerphone")), event.path)
+    self.assertEqual(None, event.build_flags)
+    self.assertEqual(None, event.purpose)
+    self.assertEqual(None, event.hs_state)
+    self.assertEqual(None, event.rend_query)
+    self.assertEqual(None, event.created)
+    self.assertEqual(None, event.reason)
+    self.assertEqual(None, event.remote_reason)
+    
+    event = _get_event(CIRC_BUILT_OLD)
+    
+    self.assertTrue(isinstance(event, stem.response.events.CircuitEvent))
+    self.assertEqual(CIRC_BUILT_OLD.lstrip("650 "), str(event))
+    self.assertEqual("1", event.id)
+    self.assertEqual(CircStatus.BUILT, event.status)
+    self.assertEqual((("E57A476CD4DFBD99B4EE52A100A58610AD6E80B9", None), (None,"hamburgerphone"), (None, "PrivacyRepublic14")), event.path)
+    self.assertEqual(None, event.build_flags)
+    self.assertEqual(None, event.purpose)
+    self.assertEqual(None, event.hs_state)
+    self.assertEqual(None, event.rend_query)
+    self.assertEqual(None, event.created)
+    self.assertEqual(None, event.reason)
+    self.assertEqual(None, event.remote_reason)
+  
   def test_bw_event(self):
     event = _get_event("650 BW 15 25")
     
diff --git a/test/unit/util/str_tools.py b/test/unit/util/str_tools.py
index 3d42154..e91e324 100644
--- a/test/unit/util/str_tools.py
+++ b/test/unit/util/str_tools.py
@@ -2,7 +2,9 @@
 Unit tests for the stem.util.str_tools functions.
 """
 
+import datetime
 import unittest
+
 from stem.util import str_tools
 
 class TestStrTools(unittest.TestCase):
@@ -118,4 +120,33 @@ class TestStrTools(unittest.TestCase):
     self.assertRaises(ValueError, str_tools.parse_short_time_label, '05:')
     self.assertRaises(ValueError, str_tools.parse_short_time_label, '05a:00')
     self.assertRaises(ValueError, str_tools.parse_short_time_label, '-05:00')
+  
+  def test_parse_iso_timestamp(self):
+    """
+    Checks the parse_iso_timestamp() function.
+    """
+    
+    test_inputs = {
+      '2012-11-08T16:48:41.420251':
+        datetime.datetime(2012, 11, 8, 16, 48, 41, 420251),
+      '2012-11-08T16:48:41.000000':
+        datetime.datetime(2012, 11, 8, 16, 48, 41, 0),
+      '2012-11-08T16:48:41':
+        datetime.datetime(2012, 11, 8, 16, 48, 41, 0),
+    }
+    
+    for arg, expected in test_inputs.items():
+      self.assertEqual(expected, str_tools.parse_iso_timestamp(arg))
+    
+    invalid_input = [
+      None,
+      32,
+      'hello world',
+      '2012-11-08T16:48:41.42025',   # too few microsecond digits
+      '2012-11-08T16:48:41.4202511', # too many microsecond digits
+      '2012-11-08T16:48',
+    ]
+    
+    for arg in invalid_input:
+      self.assertRaises(ValueError, str_tools.parse_iso_timestamp, arg)
 





More information about the tor-commits mailing list