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