
commit 3356279c284bdf9c2611708cf61b621f4cac72b0 Author: Damian Johnson <atagar@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): """