
commit 5898557b596c1e2dfbd89c44a621aefb5286955a Author: Damian Johnson <atagar@torproject.org> Date: Sun Nov 3 19:35:38 2013 -0800 Adding support for CELL_STATS events Yet another event type from... https://gitweb.torproject.org/torspec.git/commitdiff/6f2919a --- docs/change_log.rst | 1 + stem/response/events.py | 102 +++++++++++++++++++++++++++++++++++++++++- stem/version.py | 2 + test/unit/response/events.py | 59 ++++++++++++++++++++++++ 4 files changed, 162 insertions(+), 2 deletions(-) diff --git a/docs/change_log.rst b/docs/change_log.rst index f066b9b..5c6fed2 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -44,6 +44,7 @@ The following are only available within stem's `git repository * Added the id attribute to the :class:`~stem.response.events.ORConnEvent` (:spec:`6f2919a`) * Added `support for CONN_BW events <api/response.html#stem.response.events.ConnectionBandwidthEvent>`_ (:spec:`6f2919a`) * Added `support for CIRC_BW events <api/response.html#stem.response.events.CircuitBandwidthEvent>`_ (:spec:`6f2919a`) + * Added `support for CELL_STATS events <api/response.html#stem.response.events.CellStatsEvent>`_ (:spec:`6f2919a`) .. _version_1.1: diff --git a/stem/response/events.py b/stem/response/events.py index b892649..616bbfc 100644 --- a/stem/response/events.py +++ b/stem/response/events.py @@ -20,6 +20,7 @@ from stem.util import connection, log, str_tools, tor_tools KW_ARG = re.compile("^(.*) ([A-Za-z0-9_]+)=(\S*)$") QUOTED_KW_ARG = re.compile("^(.*) ([A-Za-z0-9_]+)=\"(.*)\"$") +CELL_TYPE = re.compile("^[a-z0-9_]+$") class Event(stem.response.ControlMessage): @@ -1008,15 +1009,114 @@ class CircuitBandwidthEvent(Event): self.read = long(self.read) self.written = long(self.written) + +class CellStatsEvent(Event): + """ + Event emitted every second with a count of the number of cells types broken + down by the circuit. **These events are only emitted if TestingTorNetwork is + set.** + + The CELL_STATS event was introduced in tor version 0.2.5.2-alpha. + + .. versionadded:: 1.1.0-dev + + :var str id: circuit identifier + :var str inbound_queue: inbound queue identifier + :var str inbound_connection: inbound connection identifier + :var dict inbound_added: mapping of added inbound cell types to their count + :var dict inbound_removed: mapping of removed inbound cell types to their count + :var dict inbound_time: mapping of inbound cell types to the time they took to write in milliseconds + :var str outbound_queue: outbound queue identifier + :var str outbound_connection: outbound connection identifier + :var dict outbound_added: mapping of added outbound cell types to their count + :var dict outbound_removed: mapping of removed outbound cell types to their count + :var dict outbound_time: mapping of outbound cell types to the time they took to write in milliseconds + """ + + _KEYWORD_ARGS = { + "ID": "id", + "InboundQueue": "inbound_queue", + "InboundConn": "inbound_connection", + "InboundAdded": "inbound_added", + "InboundRemoved": "inbound_removed", + "InboundTime": "inbound_time", + "OutboundQueue": "outbound_queue", + "OutboundConn": "outbound_connection", + "OutboundAdded": "outbound_added", + "OutboundRemoved": "outbound_removed", + "OutboundTime": "outbound_time", + } + + _VERSION_ADDED = stem.version.Requirement.EVENT_CELL_STATS + + def _parse(self): + if self.id 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)) + elif self.inbound_queue and not tor_tools.is_valid_circuit_id(self.inbound_queue): + raise stem.ProtocolError("Queue IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.inbound_queue, self)) + elif self.inbound_connection and not tor_tools.is_valid_connection_id(self.inbound_connection): + raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.inbound_connection, self)) + elif self.outbound_queue and not tor_tools.is_valid_circuit_id(self.outbound_queue): + raise stem.ProtocolError("Queue IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.outbound_queue, self)) + elif self.outbound_connection and not tor_tools.is_valid_connection_id(self.outbound_connection): + raise stem.ProtocolError("Connection IDs must be one to sixteen alphanumeric characters, got '%s': %s" % (self.outbound_connection, self)) + + self.inbound_added = _parse_cell_type_mapping(self.inbound_added) + self.inbound_removed = _parse_cell_type_mapping(self.inbound_removed) + self.inbound_time = _parse_cell_type_mapping(self.inbound_time) + self.outbound_added = _parse_cell_type_mapping(self.outbound_added) + self.outbound_removed = _parse_cell_type_mapping(self.outbound_removed) + self.outbound_time = _parse_cell_type_mapping(self.outbound_time) + + +def _parse_cell_type_mapping(mapping): + """ + Parses a mapping of the form... + + key1:value1,key2:value2... + + ... in which keys are strings and values are integers. + + :param str mapping: value to be parsed + + :returns: dict of **str => int** mappings + + :rasies: **stem.ProtocolError** if unable to parse the mapping + """ + + if mapping is None: + return None + + results = {} + + for entry in mapping.split(','): + if not ':' in entry: + raise stem.ProtocolError("Mappings are expected to be of the form 'key:value', got '%s': %s" % (entry, mapping)) + + key, value = entry.split(':', 1) + + if not CELL_TYPE.match(key): + raise stem.ProtocolError("Key had invalid characters, got '%s': %s" % (key, mapping)) + elif not value.isdigit(): + raise stem.ProtocolError("Values should just be integers, got '%s': %s" % (value, mapping)) + + results[key] = int(value) + + return results + + EVENT_TYPE_TO_CLASS = { "ADDRMAP": AddrMapEvent, "AUTHDIR_NEWDESCS": AuthDirNewDescEvent, "BUILDTIMEOUT_SET": BuildTimeoutSetEvent, "BW": BandwidthEvent, + "CELL_STATS": CellStatsEvent, "CIRC": CircuitEvent, + "CIRC_BW": CircuitBandwidthEvent, "CIRC_MINOR": CircMinorEvent, "CLIENTS_SEEN": ClientsSeenEvent, "CONF_CHANGED": ConfChangedEvent, + "CONN_BW": ConnectionBandwidthEvent, "DEBUG": LogEvent, "DESCCHANGED": DescChangedEvent, "ERR": LogEvent, @@ -1034,8 +1134,6 @@ EVENT_TYPE_TO_CLASS = { "STREAM": StreamEvent, "STREAM_BW": StreamBwEvent, "TRANSPORT_LAUNCHED": TransportLaunchedEvent, - "CONN_BW": ConnectionBandwidthEvent, - "CIRC_BW": CircuitBandwidthEvent, "WARN": LogEvent, # accounting for a bug in tor 0.2.0.22 diff --git a/stem/version.py b/stem/version.py index b725ade..ef1f290 100644 --- a/stem/version.py +++ b/stem/version.py @@ -45,6 +45,7 @@ easily parsed and compared, for instance... **EVENT_TRANSPORT_LAUNCHED** TRANSPORT_LAUNCHED events **EVENT_CONN_BW** CONN_BW events **EVENT_CIRC_BW** CIRC_BW events + **EVENT_CELL_STATS** CELL_STATS events **EXTENDCIRCUIT_PATH_OPTIONAL** EXTENDCIRCUIT queries can omit the path if the circuit is zero **FEATURE_EXTENDED_EVENTS** 'EXTENDED_EVENTS' optional feature **FEATURE_VERBOSE_NAMES** 'VERBOSE_NAMES' optional feature @@ -345,6 +346,7 @@ Requirement = stem.util.enum.Enum( ("EVENT_TRANSPORT_LAUNCHED", Version('0.2.5.0-alpha')), ("EVENT_CONN_BW", Version('0.2.5.2-alpha')), ("EVENT_CIRC_BW", Version('0.2.5.2-alpha')), + ("EVENT_CELL_STATS", Version('0.2.5.2-alpha')), ("EXTENDCIRCUIT_PATH_OPTIONAL", Version("0.2.2.9")), ("FEATURE_EXTENDED_EVENTS", Version("0.2.2.1-alpha")), ("FEATURE_VERBOSE_NAMES", Version("0.2.2.1-alpha")), diff --git a/test/unit/response/events.py b/test/unit/response/events.py index 6b7b6f1..09fca4f 100644 --- a/test/unit/response/events.py +++ b/test/unit/response/events.py @@ -337,6 +337,26 @@ CIRC_BW = "650 CIRC_BW ID=11 READ=272 WRITTEN=817" CIRC_BW_BAD_WRITTEN_VALUE = "650 CIRC_BW ID=11 READ=272 WRITTEN=817.7" CIRC_BW_BAD_MISSING_ID = "650 CIRC_BW READ=272 WRITTEN=817" +CELL_STATS_1 = "650 CELL_STATS ID=14 \ +OutboundQueue=19403 OutboundConn=15 \ +OutboundAdded=create_fast:1,relay_early:2 \ +OutboundRemoved=create_fast:1,relay_early:2 \ +OutboundTime=create_fast:0,relay_early:0" + +CELL_STATS_2 = "650 CELL_STATS \ +InboundQueue=19403 InboundConn=32 \ +InboundAdded=relay:1,created_fast:1 \ +InboundRemoved=relay:1,created_fast:1 \ +InboundTime=relay:0,created_fast:0 \ +OutboundQueue=6710 OutboundConn=18 \ +OutboundAdded=create:1,relay_early:1 \ +OutboundRemoved=create:1,relay_early:1 \ +OutboundTime=create:0,relay_early:0" + +CELL_STATS_BAD_1 = "650 CELL_STATS OutboundAdded=create_fast:-1,relay_early:2" +CELL_STATS_BAD_2 = "650 CELL_STATS OutboundAdded=create_fast:arg,relay_early:-2" +CELL_STATS_BAD_3 = "650 CELL_STATS OutboundAdded=create_fast!:1,relay_early:-2" + def _get_event(content): controller_event = mocking.get_message(content) @@ -1223,6 +1243,45 @@ class TestEvents(unittest.TestCase): self.assertRaises(ProtocolError, _get_event, CIRC_BW_BAD_WRITTEN_VALUE) self.assertRaises(ProtocolError, _get_event, CIRC_BW_BAD_MISSING_ID) + def test_cell_stats_event(self): + event = _get_event(CELL_STATS_1) + + self.assertTrue(isinstance(event, stem.response.events.CellStatsEvent)) + self.assertEqual(CELL_STATS_1.lstrip("650 "), str(event)) + self.assertEqual("14", event.id) + self.assertEqual(None, event.inbound_queue) + self.assertEqual(None, event.inbound_connection) + self.assertEqual(None, event.inbound_added) + self.assertEqual(None, event.inbound_removed) + self.assertEqual(None, event.inbound_time) + self.assertEqual("19403", event.outbound_queue) + self.assertEqual("15", event.outbound_connection) + self.assertEqual({'create_fast': 1, 'relay_early' :2}, event.outbound_added) + self.assertEqual({'create_fast': 1, 'relay_early': 2}, event.outbound_removed) + self.assertEqual({'create_fast': 0, 'relay_early': 0}, event.outbound_time) + + event = _get_event(CELL_STATS_2) + + self.assertTrue(isinstance(event, stem.response.events.CellStatsEvent)) + self.assertEqual(CELL_STATS_2.lstrip("650 "), str(event)) + self.assertEqual(None, event.id) + self.assertEqual("19403", event.inbound_queue) + self.assertEqual("32", event.inbound_connection) + self.assertEqual({'relay': 1, 'created_fast': 1}, event.inbound_added) + self.assertEqual({'relay': 1, 'created_fast': 1}, event.inbound_removed) + self.assertEqual({'relay': 0, 'created_fast': 0}, event.inbound_time) + self.assertEqual("6710", event.outbound_queue) + self.assertEqual("18", event.outbound_connection) + self.assertEqual({'create': 1, 'relay_early': 1}, event.outbound_added) + self.assertEqual({'create': 1, 'relay_early': 1}, event.outbound_removed) + self.assertEqual({'create': 0, 'relay_early': 0}, event.outbound_time) + + # check a few invalid mappings (bad key or value) + + self.assertRaises(ProtocolError, _get_event, CELL_STATS_BAD_1) + self.assertRaises(ProtocolError, _get_event, CELL_STATS_BAD_2) + self.assertRaises(ProtocolError, _get_event, CELL_STATS_BAD_3) + def test_unrecognized_enum_logging(self): """ Checks that when event parsing gets a value that isn't recognized by stem's