[tor-commits] [stem/master] Descriptor protocol support

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


commit 7486132886310873632ffa40a38d883bdc2b5ac7
Author: Damian Johnson <atagar at torproject.org>
Date:   Fri Dec 23 10:21:05 2016 -0800

    Descriptor protocol support
    
    Adding support for the new protocol descriptor fields...
    
      https://gitweb.torproject.org/torspec.git/commit/?id=eb4fb3c5
---
 docs/change_log.rst                               |  4 ++
 stem/descriptor/__init__.py                       | 75 +++++++++++++++++++++++
 stem/descriptor/microdescriptor.py                |  9 +++
 stem/descriptor/networkstatus.py                  | 30 +++++++++
 stem/descriptor/router_status_entry.py            |  9 +++
 stem/descriptor/server_descriptor.py              |  9 +++
 test/settings.cfg                                 |  1 +
 test/unit/descriptor/microdescriptor.py           | 10 +++
 test/unit/descriptor/networkstatus/document_v3.py | 22 +++++++
 test/unit/descriptor/protocol.py                  | 46 ++++++++++++++
 test/unit/descriptor/router_status_entry.py       |  5 ++
 test/unit/descriptor/server_descriptor.py         |  9 +++
 12 files changed, 229 insertions(+)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index d6ae108..713ef51 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -43,6 +43,10 @@ Unreleased
 The following are only available within Stem's `git repository
 <download.html>`_.
 
+ * **Descriptors**
+
+  * Support for protocol descriptor fields (:spec:`eb4fb3c5`)
+
 .. _version_1.5:
 
 Version 1.5 (November 20th, 2016)
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index fc44843..208741a 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -38,6 +38,7 @@ Package for parsing and processing descriptor data.
 
 import base64
 import codecs
+import collections
 import copy
 import hashlib
 import os
@@ -86,6 +87,72 @@ 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
@@ -389,6 +456,14 @@ def _parse_forty_character_hex(keyword, attribute):
   return _parse
 
 
+def _parse_protocol_line(keyword, attribute):
+  def _parse(descriptor, entries):
+    value = _value(keyword, entries)
+    setattr(descriptor, attribute, ProtocolSupport(keyword, value))
+
+  return _parse
+
+
 def _parse_key_block(keyword, attribute, expected_block_type, value_attribute = None):
   def _parse(descriptor, entries):
     value, block_type, block_contents = entries[keyword][0]
diff --git a/stem/descriptor/microdescriptor.py b/stem/descriptor/microdescriptor.py
index d8f8cd0..e1bab9c 100644
--- a/stem/descriptor/microdescriptor.py
+++ b/stem/descriptor/microdescriptor.py
@@ -74,6 +74,7 @@ from stem.descriptor import (
   _read_until_keywords,
   _values,
   _parse_simple_line,
+  _parse_protocol_line,
   _parse_key_block,
 )
 
@@ -98,6 +99,7 @@ SINGLE_FIELDS = (
   'family',
   'p',
   'p6',
+  'pr',
 )
 
 
@@ -187,6 +189,7 @@ _parse_onion_key_line = _parse_key_block('onion-key', 'onion_key', 'RSA PUBLIC K
 _parse_ntor_onion_key_line = _parse_simple_line('ntor-onion-key', 'ntor_onion_key')
 _parse_family_line = _parse_simple_line('family', 'family', func = lambda v: v.split(' '))
 _parse_p6_line = _parse_simple_line('p6', 'exit_policy_v6', func = lambda v: stem.exit_policy.MicroExitPolicy(v))
+_parse_pr_line = _parse_protocol_line('pr', 'protocols')
 
 
 class Microdescriptor(Descriptor):
@@ -208,6 +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 str identifier: base64 encoded identity digest (**deprecated**, use
     identifiers instead)
@@ -222,6 +226,9 @@ class Microdescriptor(Descriptor):
   .. versionchanged:: 1.5.0
      Added the identifiers attribute, and deprecated identifier and
      identifier_type since the field can now appear multiple times.
+
+  .. versionchanged:: 1.6.0
+     Added the protocols attribute.
   """
 
   ATTRIBUTES = {
@@ -234,6 +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),
     'digest': (None, _parse_digest),
   }
 
@@ -244,6 +252,7 @@ class Microdescriptor(Descriptor):
     'family': _parse_family_line,
     'p': _parse_p_line,
     'p6': _parse_p6_line,
+    'pr': _parse_pr_line,
     'id': _parse_id_line,
   }
 
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 7456b37..75ec459 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -73,6 +73,7 @@ from stem.descriptor import (
   _parse_if_present,
   _parse_timestamp_line,
   _parse_forty_character_hex,
+  _parse_protocol_line,
   _parse_key_block,
 )
 
@@ -123,6 +124,10 @@ HEADER_STATUS_DOCUMENT_FIELDS = (
   ('shared-rand-commit', True, False, False),
   ('shared-rand-previous-value', True, True, False),
   ('shared-rand-current-value', True, True, False),
+  ('recommended-client-protocols', True, True, False),
+  ('recommended-relay-protocols', True, True, False),
+  ('required-client-protocols', True, True, False),
+  ('required-relay-protocols', True, True, False),
   ('params', True, True, False),
 )
 
@@ -756,6 +761,10 @@ _parse_header_server_versions_line = _parse_versions_line('server-versions', 'se
 _parse_header_known_flags_line = _parse_simple_line('known-flags', 'known_flags', func = lambda v: [entry for entry in v.split(' ') if entry])
 _parse_footer_bandwidth_weights_line = _parse_simple_line('bandwidth-weights', 'bandwidth_weights', func = lambda v: _parse_int_mappings('bandwidth-weights', v, True))
 _parse_shared_rand_participate_line = _parse_if_present('shared-rand-participate', 'is_shared_randomness_participate')
+_parse_recommended_client_protocols_line = _parse_protocol_line('recommended-client-protocols', 'recommended_client_protocols')
+_parse_recommended_relay_protocols_line = _parse_protocol_line('recommended-relay-protocols', 'recommended_relay_protocols')
+_parse_required_client_protocols_line = _parse_protocol_line('required-client-protocols', 'required_client_protocols')
+_parse_required_relay_protocols_line = _parse_protocol_line('required-relay-protocols', 'required_relay_protocols')
 
 
 class NetworkStatusDocumentV3(NetworkStatusDocument):
@@ -812,6 +821,15 @@ 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
+
   **\*** attribute is either required when we're parsed with validation or has
   a default value, others are left as None if undefined
 
@@ -824,6 +842,10 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
      shared_randomness_previous_value,
      shared_randomness_current_reveal_count, and
      shared_randomness_current_value attributes.
+
+  .. versionchanged:: 1.6.0
+     Added the recommended_client_protocols, recommended_relay_protocols,
+     required_client_protocols, and required_relay_protocols.
   """
 
   ATTRIBUTES = {
@@ -851,6 +873,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),
     'params': ({}, _parse_header_parameters_line),
 
     'signatures': ([], _parse_footer_directory_signature_line),
@@ -876,6 +902,10 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
     'shared-rand-commit': _parsed_shared_rand_commit,
     'shared-rand-previous-value': _parse_shared_rand_previous_value,
     'shared-rand-current-value': _parse_shared_rand_current_value,
+    '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,
   }
 
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index bf146a9..abdefa2 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -32,9 +32,12 @@ from stem.descriptor import (
   _value,
   _values,
   _get_descriptor_components,
+  _parse_protocol_line,
   _read_until_keywords,
 )
 
+_parse_pr_line = _parse_protocol_line('pr', 'protocols')
+
 
 def _parse_file(document_file, validate, entry_class, entry_keyword = 'r', start_position = None, end_position = None, section_end_keywords = (), extra_args = ()):
   """
@@ -560,6 +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 list microdescriptor_hashes: **\*** tuples of two values, the list of
     consensus methods for generating a set of digests and the 'algorithm =>
@@ -570,6 +574,9 @@ class RouterStatusEntryV3(RouterStatusEntry):
 
   .. versionchanged:: 1.5.0
      Added the identifier and identifier_type attributes.
+
+  .. versionchanged:: 1.6.0
+     Added the protocols attribute.
   """
 
   ATTRIBUTES = dict(RouterStatusEntry.ATTRIBUTES, **{
@@ -584,6 +591,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
     'unrecognized_bandwidth_entries': ([], _parse_w_line),
 
     'exit_policy': (None, _parse_p_line),
+    'pr': (None, _parse_pr_line),
     'microdescriptor_hashes': ([], _parse_m_line),
   })
 
@@ -591,6 +599,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
     'a': _parse_a_line,
     'w': _parse_w_line,
     'p': _parse_p_line,
+    'pr': _parse_pr_line,
     'id': _parse_id_line,
     'm': _parse_m_line,
   })
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index 9f7d8d1..ffd0183 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -58,6 +58,7 @@ from stem.descriptor import (
   _parse_bytes_line,
   _parse_timestamp_line,
   _parse_forty_character_hex,
+  _parse_protocol_line,
   _parse_key_block,
 )
 
@@ -96,6 +97,7 @@ SINGLE_FIELDS = (
   'protocols',
   'allow-single-hop-exits',
   'tunnelled-dir-server',
+  'proto',
   'onion-key-crosscert',
   'ntor-onion-key',
   'ntor-onion-key-crosscert',
@@ -394,6 +396,7 @@ _parse_write_history_line = functools.partial(_parse_history_line, 'write-histor
 _parse_ipv6_policy_line = _parse_simple_line('ipv6-policy', 'exit_policy_v6', func = lambda v: stem.exit_policy.MicroExitPolicy(v))
 _parse_allow_single_hop_exits_line = _parse_if_present('allow-single-hop-exits', 'allow_single_hop_exits')
 _parse_tunneled_dir_server_line = _parse_if_present('tunnelled-dir-server', 'allow_tunneled_dir_requests')
+_parse_proto_line = _parse_protocol_line('proto', 'protocols')
 _parse_caches_extra_info_line = _parse_if_present('caches-extra-info', 'extra_info_cache')
 _parse_family_line = _parse_simple_line('family', 'family', func = lambda v: set(v.split(' ')))
 _parse_eventdns_line = _parse_simple_line('eventdns', 'eventdns', func = lambda v: v == '1')
@@ -447,6 +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
 
   **Deprecated**, moved to extra-info descriptor...
 
@@ -463,6 +467,9 @@ class ServerDescriptor(Descriptor):
 
   .. versionchanged:: 1.5.0
      Added the allow_tunneled_dir_requests attribute.
+
+  .. versionchanged:: 1.6.0
+     Added the protocols attribute.
   """
 
   ATTRIBUTES = {
@@ -493,6 +500,7 @@ class ServerDescriptor(Descriptor):
     'hibernating': (False, _parse_hibernating_line),
     'allow_single_hop_exits': (False, _parse_allow_single_hop_exits_line),
     'allow_tunneled_dir_requests': (False, _parse_tunneled_dir_server_line),
+    'protocols': ({}, _parse_proto_line),
     'extra_info_cache': (False, _parse_caches_extra_info_line),
     'extra_info_digest': (None, _parse_extrainfo_digest_line),
     'hidden_service_dir': (None, _parse_hidden_service_dir_line),
@@ -528,6 +536,7 @@ class ServerDescriptor(Descriptor):
     'ipv6-policy': _parse_ipv6_policy_line,
     'allow-single-hop-exits': _parse_allow_single_hop_exits_line,
     'tunnelled-dir-server': _parse_tunneled_dir_server_line,
+    'proto': _parse_proto_line,
     'caches-extra-info': _parse_caches_extra_info_line,
     'family': _parse_family_line,
     'eventdns': _parse_eventdns_line,
diff --git a/test/settings.cfg b/test/settings.cfg
index 4913202..627c270 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -179,6 +179,7 @@ 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 10ccd15..9d4d4fc 100644
--- a/test/unit/descriptor/microdescriptor.py
+++ b/test/unit/descriptor/microdescriptor.py
@@ -98,6 +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.get_unrecognized_lines())
 
   def test_unrecognized_line(self):
@@ -165,6 +166,15 @@ class TestMicrodescriptor(unittest.TestCase):
     desc = get_microdescriptor({'p': 'accept 80,110,143,443'})
     self.assertEqual(stem.exit_policy.MicroExitPolicy('accept 80,110,143,443'), desc.exit_policy)
 
+  def test_protocols(self):
+    """
+    Basic check for 'pr' lines.
+    """
+
+    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'))
+
   def test_identifier(self):
     """
     Basic check for 'id' lines.
diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py
index aae12a6..09faf43 100644
--- a/test/unit/descriptor/networkstatus/document_v3.py
+++ b/test/unit/descriptor/networkstatus/document_v3.py
@@ -343,6 +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(DEFAULT_PARAMS, document.params)
     self.assertEqual((), document.directory_authorities)
     self.assertEqual({}, document.bandwidth_weights)
@@ -926,6 +930,24 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w=
       self.assertEqual(None, document.shared_randomness_current_reveal_count)
       self.assertEqual(None, document.shared_randomness_current_value)
 
+  def test_parameters(self):
+    """
+    Parses the parameters attributes.
+    """
+
+    document = get_network_status_document_v3(OrderedDict([
+      ('vote-status', 'vote'),
+      ('recommended-client-protocols', 'HSDir=1 HSIntro=3'),
+      ('recommended-relay-protocols', 'Cons=1 Desc=1'),
+      ('required-client-protocols', 'HSRend=1 Link=1-4 LinkAuth=1 Microdesc=1'),
+      ('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)))
+
   def test_params(self):
     """
     General testing for the 'params' line, exercising the happy cases.
diff --git a/test/unit/descriptor/protocol.py b/test/unit/descriptor/protocol.py
new file mode 100644
index 0000000..a47d7c6
--- /dev/null
+++ b/test/unit/descriptor/protocol.py
@@ -0,0 +1,46 @@
+"""
+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 25ba99d..b8597be 100644
--- a/test/unit/descriptor/router_status_entry.py
+++ b/test/unit/descriptor/router_status_entry.py
@@ -482,6 +482,11 @@ class TestRouterStatusEntry(unittest.TestCase):
       content = get_router_status_entry_v3({'s': s_line}, content = True)
       self._expect_invalid_attr(content, 'flags', expected)
 
+  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'))
+
   def test_versions(self):
     """
     Handles a variety of version inputs.
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index 834d1ea..d84ad48 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -657,6 +657,15 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     desc = get_relay_server_descriptor({'ipv6-policy': 'accept 22-23,53,80,110'})
     self.assertEqual(expected, desc.exit_policy_v6)
 
+  def test_protocols(self):
+    """
+    Checks a 'proto' line.
+    """
+
+    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'))
+
   def test_ntor_onion_key(self):
     """
     Checks a 'ntor-onion-key' line.





More information about the tor-commits mailing list