[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Support for CIRC events
commit a349a01fece5534195a6619d72df4944065001b8
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
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)
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits