[tor-commits] [stem/master] Support protocols with commas

atagar at torproject.org atagar at torproject.org
Sun Dec 25 18:40:59 UTC 2016


commit 3356279c284bdf9c2611708cf61b621f4cac72b0
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Dec 25 10:19:19 2016 -0800

    Support protocols with commas
    
    Oops! Thought all protocols were contiguous ranges. Turns out they can have
    commas...
    
      MyProtocol=1,3
    
    If that's the case then a dictionary of lists makes more sense than the class
    we had. With this protocol checks can be done like...
    
      if 4 in desc.protocols.get('MyProtocol', []):
---
 stem/descriptor/__init__.py                       | 93 ++++++-----------------
 stem/descriptor/microdescriptor.py                |  4 +-
 stem/descriptor/networkstatus.py                  | 20 ++---
 stem/descriptor/router_status_entry.py            | 14 +++-
 stem/descriptor/server_descriptor.py              |  2 +-
 test/settings.cfg                                 |  1 -
 test/unit/descriptor/microdescriptor.py           |  5 +-
 test/unit/descriptor/networkstatus/document_v3.py | 16 ++--
 test/unit/descriptor/protocol.py                  | 46 -----------
 test/unit/descriptor/router_status_entry.py       |  3 +-
 test/unit/descriptor/server_descriptor.py         | 25 +++++-
 11 files changed, 80 insertions(+), 149 deletions(-)

diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index 208741a..d7b1aea 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -38,7 +38,6 @@ Package for parsing and processing descriptor data.
 
 import base64
 import codecs
-import collections
 import copy
 import hashlib
 import os
@@ -87,72 +86,6 @@ DocumentHandler = stem.util.enum.UppercaseEnum(
 )
 
 
-class ProtocolSupport(object):
-  """
-  Protocols supported by a relay.
-
-  .. versionadded:: 1.6.0
-  """
-
-  def __init__(self, keyword, value):
-    # parses 'protocol' entries like: Cons=1-2 Desc=1-2 DirCache=1 HSDir=1
-
-    self._entries = OrderedDict()
-
-    for entry in value.split():
-      if '=' not in entry:
-        raise ValueError("Protocol entires are expected to be a series of 'key=value' pairs but was: %s %s" % (keyword, value))
-
-      k, v = entry.split('=', 1)
-
-      if '-' in v:
-        min_value, max_value = v.split('-', 1)
-      else:
-        min_value = max_value = v
-
-      if not min_value.isdigit() or not max_value.isdigit():
-        raise ValueError('Protocol values should be a number or number range, but was: %s %s' % (keyword, value))
-
-      self._entries[k] = Protocol(k, int(min_value), int(max_value))
-
-  def is_supported(self, protocol, version = None):
-    """
-    Checks if the given protocol is supported.
-
-    :param str protocol: protocol to check support of (DirCache, HSDir, etc)
-    :param int version: protocol version to check support of
-
-    :returns: **True** if the protocol is supported, **False** otherwise
-    """
-
-    supported = self._entries.get(protocol)
-
-    if not supported:
-      return False
-    elif version and version < supported.min_version:
-      return False
-    elif version and version > supported.max_version:
-      return False
-    else:
-      return True
-
-  def __iter__(self):
-    for protocol in self._entries.values():
-      yield protocol
-
-
-class Protocol(collections.namedtuple('Protocol', ['name', 'min_version', 'max_version'])):
-  """
-  Individual protocol range supported by a relay.
-
-  .. versionadded:: 1.6.0
-
-  :var str name: protocol name (ex. DirCache, HSDir, etc)
-  :var int min_version: minimum protocol supported
-  :var int max_version: maximum protocol supported
-  """
-
-
 def parse_file(descriptor_file, descriptor_type = None, validate = False, document_handler = DocumentHandler.ENTRIES, normalize_newlines = None, **kwargs):
   """
   Simple function to read the descriptor contents from a file, providing an
@@ -458,8 +391,32 @@ def _parse_forty_character_hex(keyword, attribute):
 
 def _parse_protocol_line(keyword, attribute):
   def _parse(descriptor, entries):
+    # parses 'protocol' entries like: Cons=1-2 Desc=1-2 DirCache=1 HSDir=1
+
     value = _value(keyword, entries)
-    setattr(descriptor, attribute, ProtocolSupport(keyword, value))
+    protocols = OrderedDict()
+
+    for entry in value.split():
+      if '=' not in entry:
+        raise ValueError("Protocol entires are expected to be a series of 'key=value' pairs but was: %s %s" % (keyword, value))
+
+      k, v = entry.split('=', 1)
+      versions = []
+
+      for subentry in v.split(','):
+        if '-' in subentry:
+          min_value, max_value = subentry.split('-', 1)
+        else:
+          min_value = max_value = subentry
+
+        if not min_value.isdigit() or not max_value.isdigit():
+          raise ValueError('Protocol values should be a number or number range, but was: %s %s' % (keyword, value))
+
+        versions += range(int(min_value), int(max_value) + 1)
+
+      protocols[k] = versions
+
+    setattr(descriptor, attribute, protocols)
 
   return _parse
 
diff --git a/stem/descriptor/microdescriptor.py b/stem/descriptor/microdescriptor.py
index e1bab9c..0f1a015 100644
--- a/stem/descriptor/microdescriptor.py
+++ b/stem/descriptor/microdescriptor.py
@@ -211,7 +211,7 @@ class Microdescriptor(Descriptor):
   :var hash identifiers: mapping of key types (like rsa1024 or ed25519) to
     their base64 encoded identity, this is only used for collision prevention
     (:trac:`11743`)
-  :var stem.descriptor.ProtocolSupport protocols: supported protocols
+  :var dict protocols: mapping of protocols to their supported versions
 
   :var str identifier: base64 encoded identity digest (**deprecated**, use
     identifiers instead)
@@ -241,7 +241,7 @@ class Microdescriptor(Descriptor):
     'identifier_type': (None, _parse_id_line),  # deprecated in favor of identifiers
     'identifier': (None, _parse_id_line),  # deprecated in favor of identifiers
     'identifiers': ({}, _parse_id_line),
-    'protocols': (None, _parse_pr_line),
+    'protocols': ({}, _parse_pr_line),
     'digest': (None, _parse_digest),
   }
 
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 75ec459..4e97d67 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -821,14 +821,10 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
   :var str shared_randomness_current_value: base64 encoded current shared
     random value
 
-  :var stem.descriptor.ProtocolSupport recommended_client_protocols: recommended
-    protocols for clients
-  :var stem.descriptor.ProtocolSupport recommended_relay_protocols: recommended
-    protocols for relays
-  :var stem.descriptor.ProtocolSupport required_client_protocols: required
-    protocols for clients
-  :var stem.descriptor.ProtocolSupport required_relay_protocols: required
-    protocols for relays
+  :var dict recommended_client_protocols: recommended protocols for clients
+  :var dict recommended_relay_protocols: recommended protocols for relays
+  :var dict required_client_protocols: required protocols for clients
+  :var dict required_relay_protocols: required protocols for relays
 
   **\*** attribute is either required when we're parsed with validation or has
   a default value, others are left as None if undefined
@@ -873,10 +869,10 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
     'shared_randomness_previous_value': (None, _parse_shared_rand_previous_value),
     'shared_randomness_current_reveal_count': (None, _parse_shared_rand_current_value),
     'shared_randomness_current_value': (None, _parse_shared_rand_current_value),
-    'recommended_client_protocols': (None, _parse_recommended_client_protocols_line),
-    'recommended_relay_protocols': (None, _parse_recommended_relay_protocols_line),
-    'required_client_protocols': (None, _parse_required_client_protocols_line),
-    'required_relay_protocols': (None, _parse_required_relay_protocols_line),
+    'recommended_client_protocols': ({}, _parse_recommended_client_protocols_line),
+    'recommended_relay_protocols': ({}, _parse_recommended_relay_protocols_line),
+    'required_client_protocols': ({}, _parse_required_client_protocols_line),
+    'required_relay_protocols': ({}, _parse_required_relay_protocols_line),
     'params': ({}, _parse_header_parameters_line),
 
     'signatures': ([], _parse_footer_directory_signature_line),
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index abdefa2..98fc042 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -563,7 +563,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
     information that isn't yet recognized
 
   :var stem.exit_policy.MicroExitPolicy exit_policy: router's exit policy
-  :var stem.descriptor.ProtocolSupport protocols: supported protocols
+  :var dict protocols: mapping of protocols to their supported versions
 
   :var list microdescriptor_hashes: **\*** tuples of two values, the list of
     consensus methods for generating a set of digests and the 'algorithm =>
@@ -591,7 +591,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
     'unrecognized_bandwidth_entries': ([], _parse_w_line),
 
     'exit_policy': (None, _parse_p_line),
-    'pr': (None, _parse_pr_line),
+    'protocols': ({}, _parse_pr_line),
     'microdescriptor_hashes': ([], _parse_m_line),
   })
 
@@ -611,7 +611,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
     return ('r', 's')
 
   def _single_fields(self):
-    return ('r', 's', 'v', 'w', 'p')
+    return ('r', 's', 'v', 'w', 'p', 'pr')
 
   def _compare(self, other, method):
     if not isinstance(other, RouterStatusEntryV3):
@@ -646,9 +646,13 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
     measurements
   :var list unrecognized_bandwidth_entries: **\*** bandwidth weighting
     information that isn't yet recognized
+  :var dict protocols: mapping of protocols to their supported versions
 
   :var str digest: **\*** router's hex encoded digest of our corresponding microdescriptor
 
+  .. versionchanged:: 1.6.0
+     Added the protocols attribute.
+
   **\*** attribute is either required when we're parsed with validation or has
   a default value, others are left as **None** if undefined
   """
@@ -658,6 +662,7 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
     'measured': (None, _parse_w_line),
     'is_unmeasured': (False, _parse_w_line),
     'unrecognized_bandwidth_entries': ([], _parse_w_line),
+    'protocols': ({}, _parse_pr_line),
 
     'digest': (None, _parse_microdescriptor_m_line),
   })
@@ -665,6 +670,7 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
   PARSER_FOR_LINE = dict(RouterStatusEntry.PARSER_FOR_LINE, **{
     'w': _parse_w_line,
     'm': _parse_microdescriptor_m_line,
+    'pr': _parse_pr_line,
   })
 
   def _name(self, is_plural = False):
@@ -674,7 +680,7 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
     return ('r', 's', 'm')
 
   def _single_fields(self):
-    return ('r', 's', 'v', 'w', 'm')
+    return ('r', 's', 'v', 'w', 'm', 'pr')
 
   def _compare(self, other, method):
     if not isinstance(other, RouterStatusEntryMicroV3):
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index ffd0183..0ba28b3 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -450,7 +450,7 @@ class ServerDescriptor(Descriptor):
   :var list or_addresses: **\*** alternative for our address/or_port
     attributes, each entry is a tuple of the form (address (**str**), port
     (**int**), is_ipv6 (**bool**))
-  :var stem.descriptor.ProtocolSupport protocols: supported protocols
+  :var dict protocols: mapping of protocols to their supported versions
 
   **Deprecated**, moved to extra-info descriptor...
 
diff --git a/test/settings.cfg b/test/settings.cfg
index 627c270..4913202 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -179,7 +179,6 @@ test.unit_tests
 |test.unit.descriptor.export.TestExport
 |test.unit.descriptor.reader.TestDescriptorReader
 |test.unit.descriptor.remote.TestDescriptorDownloader
-|test.unit.descriptor.protocol.TestProtocol
 |test.unit.descriptor.server_descriptor.TestServerDescriptor
 |test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor
 |test.unit.descriptor.microdescriptor.TestMicrodescriptor
diff --git a/test/unit/descriptor/microdescriptor.py b/test/unit/descriptor/microdescriptor.py
index 9d4d4fc..9b3fb2d 100644
--- a/test/unit/descriptor/microdescriptor.py
+++ b/test/unit/descriptor/microdescriptor.py
@@ -98,7 +98,7 @@ class TestMicrodescriptor(unittest.TestCase):
     self.assertEqual({}, desc.identifiers)
     self.assertEqual(None, desc.identifier_type)
     self.assertEqual(None, desc.identifier)
-    self.assertEqual(None, desc.protocols)
+    self.assertEqual({}, desc.protocols)
     self.assertEqual([], desc.get_unrecognized_lines())
 
   def test_unrecognized_line(self):
@@ -172,8 +172,7 @@ class TestMicrodescriptor(unittest.TestCase):
     """
 
     desc = get_microdescriptor({'pr': 'Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2'})
-    self.assertEqual(10, len(list(desc.protocols)))
-    self.assertTrue(desc.protocols.is_supported('Desc'))
+    self.assertEqual(10, len(desc.protocols))
 
   def test_identifier(self):
     """
diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py
index 09faf43..8203e4e 100644
--- a/test/unit/descriptor/networkstatus/document_v3.py
+++ b/test/unit/descriptor/networkstatus/document_v3.py
@@ -343,10 +343,10 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
     self.assertEqual(None, document.shared_randomness_previous_value)
     self.assertEqual(None, document.shared_randomness_current_reveal_count)
     self.assertEqual(None, document.shared_randomness_current_value)
-    self.assertEqual(None, document.recommended_client_protocols)
-    self.assertEqual(None, document.recommended_relay_protocols)
-    self.assertEqual(None, document.required_client_protocols)
-    self.assertEqual(None, document.required_relay_protocols)
+    self.assertEqual({}, document.recommended_client_protocols)
+    self.assertEqual({}, document.recommended_relay_protocols)
+    self.assertEqual({}, document.required_client_protocols)
+    self.assertEqual({}, document.required_relay_protocols)
     self.assertEqual(DEFAULT_PARAMS, document.params)
     self.assertEqual((), document.directory_authorities)
     self.assertEqual({}, document.bandwidth_weights)
@@ -943,10 +943,10 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
       ('required-relay-protocols', 'DirCache=1'),
     ]))
 
-    self.assertEqual(2, len(list(document.recommended_client_protocols)))
-    self.assertEqual(2, len(list(document.recommended_relay_protocols)))
-    self.assertEqual(4, len(list(document.required_client_protocols)))
-    self.assertEqual(1, len(list(document.required_relay_protocols)))
+    self.assertEqual(2, len(document.recommended_client_protocols))
+    self.assertEqual(2, len(document.recommended_relay_protocols))
+    self.assertEqual(4, len(document.required_client_protocols))
+    self.assertEqual(1, len(document.required_relay_protocols))
 
   def test_params(self):
     """
diff --git a/test/unit/descriptor/protocol.py b/test/unit/descriptor/protocol.py
deleted file mode 100644
index a47d7c6..0000000
--- a/test/unit/descriptor/protocol.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""
-Unit tessts for the stem.descriptor.ProtocolSupport class.
-"""
-
-import unittest
-
-from stem.descriptor import Protocol, ProtocolSupport
-
-
-class TestProtocol(unittest.TestCase):
-  def test_parsing(self):
-    expected = [
-      Protocol(name = 'Desc', min_version = 1, max_version = 1),
-      Protocol(name = 'Link', min_version = 1, max_version = 4),
-      Protocol(name = 'Microdesc', min_version = 1, max_version = 1),
-      Protocol(name = 'Relay', min_version = 1, max_version = 2),
-    ]
-
-    self.assertEqual(expected, list(ProtocolSupport('pr', 'Desc=1 Link=1-4 Microdesc=1 Relay=1-2')))
-
-  def test_parse_with_no_mapping(self):
-    try:
-      ProtocolSupport('pr', 'Desc Link=1-4')
-      self.fail('Did not raise expected exception')
-    except ValueError as exc:
-      self.assertEqual("Protocol entires are expected to be a series of 'key=value' pairs but was: pr Desc Link=1-4", str(exc))
-
-  def test_parse_with_non_int_version(self):
-    try:
-      ProtocolSupport('pr', 'Desc=hi Link=1-4')
-      self.fail('Did not raise expected exception')
-    except ValueError as exc:
-      self.assertEqual('Protocol values should be a number or number range, but was: pr Desc=hi Link=1-4', str(exc))
-
-  def test_is_supported(self):
-    protocol = ProtocolSupport('pr', 'Desc=1 Link=2-4 Microdesc=1 Relay=1-2')
-    self.assertFalse(protocol.is_supported('NoSuchProtocol'))
-    self.assertFalse(protocol.is_supported('Desc', 2))
-    self.assertTrue(protocol.is_supported('Desc'))
-    self.assertTrue(protocol.is_supported('Desc', 1))
-
-    self.assertFalse(protocol.is_supported('Link', 1))
-    self.assertTrue(protocol.is_supported('Link', 2))
-    self.assertTrue(protocol.is_supported('Link', 3))
-    self.assertTrue(protocol.is_supported('Link', 4))
-    self.assertFalse(protocol.is_supported('Link', 5))
diff --git a/test/unit/descriptor/router_status_entry.py b/test/unit/descriptor/router_status_entry.py
index b8597be..fc02539 100644
--- a/test/unit/descriptor/router_status_entry.py
+++ b/test/unit/descriptor/router_status_entry.py
@@ -484,8 +484,7 @@ class TestRouterStatusEntry(unittest.TestCase):
 
   def test_protocols(self):
     desc = get_router_status_entry_v3({'pr': 'Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2'})
-    self.assertEqual(10, len(list(desc.protocols)))
-    self.assertTrue(desc.protocols.is_supported('Desc'))
+    self.assertEqual(10, len(desc.protocols))
 
   def test_versions(self):
     """
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index d84ad48..ba8271d 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -663,8 +663,29 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     """
 
     desc = get_relay_server_descriptor({'proto': 'Cons=1 Desc=1 DirCache=1 HSDir=1 HSIntro=3 HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1 Relay=1-2'})
-    self.assertEqual(10, len(list(desc.protocols)))
-    self.assertTrue(desc.protocols.is_supported('Desc'))
+    self.assertEqual({'Cons': [1], 'Desc': [1], 'DirCache': [1], 'HSDir': [1], 'HSIntro': [3], 'HSRend': [1], 'Link': [1, 2, 3, 4], 'LinkAuth': [1], 'Microdesc': [1], 'Relay': [1, 2]}, desc.protocols)
+
+  def test_protocols_with_no_mapping(self):
+    """
+    Checks a 'proto' line when it's not key=value pairs.
+    """
+
+    try:
+      get_relay_server_descriptor({'proto': 'Desc Link=1-4'})
+      self.fail('Did not raise expected exception')
+    except ValueError as exc:
+      self.assertEqual("Protocol entires are expected to be a series of 'key=value' pairs but was: proto Desc Link=1-4", str(exc))
+
+  def test_parse_with_non_int_version(self):
+    """
+    Checks a 'proto' line with non-numeric content.
+    """
+
+    try:
+      get_relay_server_descriptor({'proto': 'Desc=hi Link=1-4'})
+      self.fail('Did not raise expected exception')
+    except ValueError as exc:
+      self.assertEqual('Protocol values should be a number or number range, but was: proto Desc=hi Link=1-4', str(exc))
 
   def test_ntor_onion_key(self):
     """



More information about the tor-commits mailing list