commit 683b1ba479f2aff3c277e0ff53bdc8b1f81664af Author: Damian Johnson atagar@torproject.org Date: Mon Dec 31 01:43:00 2012 -0800
Support for consensus 'a' lines
Adding support for the 'a' lines in version 3 router status entries. These contain IPv6 addresses and port lists. The port lists will make this unweildy for users but that's out of our hands. --- stem/descriptor/router_status_entry.py | 46 ++++++++++++++++++++++--- test/unit/descriptor/router_status_entry.py | 49 +++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 8 deletions(-)
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py index 31458c1..ed01132 100644 --- a/stem/descriptor/router_status_entry.py +++ b/stem/descriptor/router_status_entry.py @@ -270,6 +270,8 @@ class RouterStatusEntryV3(RouterStatusEntry): Information about an individual router stored within a version 3 network status document.
+ :var dict addresses_v6: ***** relay's IPv6 OR addresses, this is a mapping + of IPv6 addresses to a listing of [(min port, max port)...] it accepts :var str digest: ***** router's digest
:var int bandwidth: bandwidth claimed by the relay (in kb/s) @@ -279,14 +281,16 @@ class RouterStatusEntryV3(RouterStatusEntry):
:var stem.exit_policy.MicrodescriptorExitPolicy exit_policy: router's exit policy
- :var list microdescriptor_hashes: tuples of two values, the list of consensus - methods for generating a set of digests and the 'algorithm => digest' mappings + :var list microdescriptor_hashes: ***** tuples of two values, the list of + consensus methods for generating a set of digests and the 'algorithm => + digest' mappings
***** attribute is either required when we're parsed with validation or has a default value, others are left as **None** if undefined """
def __init__(self, content, validate = True, document = None): + self.addresses_v6 = {} self.digest = None
self.bandwidth = None @@ -294,7 +298,7 @@ class RouterStatusEntryV3(RouterStatusEntry): self.unrecognized_bandwidth_entries = []
self.exit_policy = None - self.microdescriptor_hashes = None + self.microdescriptor_hashes = []
super(RouterStatusEntryV3, self).__init__(content, validate, document)
@@ -305,6 +309,11 @@ class RouterStatusEntryV3(RouterStatusEntry): if keyword == 'r': _parse_r_line(self, value, validate, True) del entries['r'] + elif keyword == 'a': + for entry, _ in values: + _parse_a_line(self, entry, validate) + + del entries['a'] elif keyword == 'w': _parse_w_line(self, value, validate) del entries['w'] @@ -449,6 +458,34 @@ def _parse_r_line(desc, value, validate, include_digest = True): if validate: raise ValueError("Publication time time wasn't parsable: r %s" % value)
+def _parse_a_line(desc, value, validate): + # "a" SP address ":" portlist + # example: a [2001:888:2133:0:82:94:251:204]:9001 + + if not ':' in value: + if not validate: return + raise ValueError("%s 'a' line must be of the form '[address]:[ports]': a %s" % (desc._name(), value)) + + address, ports = value.rsplit(':', 1) + + if validate and not stem.util.connection.is_valid_ipv6_address(address, allow_brackets = True): + raise ValueError("%s 'a' line must start with an IPv6 address: a %s" % (desc._name(), value)) + + address = address.lstrip('[').rstrip(']') + + for port_entry in ports.split(','): + if '-' in port_entry: + min_port, max_port = port_entry.split('-', 1) + else: + min_port = max_port = port_entry + + if not stem.util.connection.is_valid_port(min_port) or \ + not stem.util.connection.is_valid_port(max_port): + if not validate: continue + raise ValueError("%s 'a' line had an invalid port range (%s): a %s" % (desc._name(), port_entry, value)) + + desc.addresses_v6.setdefault(address, []).append((int(min_port), int(max_port))) + def _parse_s_line(desc, value, validate): # "s" Flags # example: s Named Running Stable Valid @@ -556,9 +593,6 @@ def _parse_m_line(desc, value, validate): hash_name, digest = entry.split('=', 1) hashes[hash_name] = digest
- if desc.microdescriptor_hashes is None: - desc.microdescriptor_hashes = [] - desc.microdescriptor_hashes.append((methods, hashes))
def _decode_fingerprint(identity, validate): diff --git a/test/unit/descriptor/router_status_entry.py b/test/unit/descriptor/router_status_entry.py index b0cd3b8..ef03baa 100644 --- a/test/unit/descriptor/router_status_entry.py +++ b/test/unit/descriptor/router_status_entry.py @@ -82,7 +82,7 @@ class TestRouterStatusEntry(unittest.TestCase): self.assertEqual(None, entry.measured) self.assertEqual([], entry.unrecognized_bandwidth_entries) self.assertEqual(None, entry.exit_policy) - self.assertEqual(None, entry.microdescriptor_hashes) + self.assertEqual([], entry.microdescriptor_hashes) self.assertEqual([], entry.get_unrecognized_lines())
def test_minimal_micro_v3(self): @@ -304,6 +304,51 @@ class TestRouterStatusEntry(unittest.TestCase): content = get_router_status_entry_v3({'r': r_line}, content = True) self._expect_invalid_attr(content, attr, expected)
+ def test_ipv6_addresses(self): + """ + Handles a variety of 'a' lines. + """ + + test_values = { + "[2607:fcd0:daaa:101::602c:bd62]:443": { + '2607:fcd0:daaa:101::602c:bd62': [(443, 443)]}, + "[2607:fcd0:daaa:101::602c:bd62]:80,443": { + '2607:fcd0:daaa:101::602c:bd62': [(80, 80), (443, 443)]}, + "[2607:fcd0:daaa:101::602c:bd62]:443-512": { + '2607:fcd0:daaa:101::602c:bd62': [(443, 512)]}, + } + + for a_line, expected in test_values.items(): + entry = get_router_status_entry_v3({'a': a_line}) + self.assertEquals(expected, entry.addresses_v6) + + # includes multiple 'a' lines + + content = get_router_status_entry_v3(content = True) + content += "\na [2607:fcd0:daaa:101::602c:bd62]:80,443" + content += "\na [2607:fcd0:daaa:101::602c:bd62]:512-600" + content += "\na [1148:fcd0:daaa:101::602c:bd62]:80" + + expected = { + '2607:fcd0:daaa:101::602c:bd62': [(80, 80), (443, 443), (512, 600)], + '1148:fcd0:daaa:101::602c:bd62': [(80, 80)], + } + + entry = RouterStatusEntryV3(content) + self.assertEquals(expected, entry.addresses_v6) + + # tries some invalid inputs + + test_values = ( + "", + "127.0.0.1:80", + "[1148:fcd0:daaa:101::602c:bd62]:80000", + ) + + for a_line in test_values: + content = get_router_status_entry_v3({'a': a_line}, content = True) + self._expect_invalid_attr(content, expected_value = {}) + def test_flags(self): """ Handles a variety of flag inputs. @@ -454,7 +499,7 @@ class TestRouterStatusEntry(unittest.TestCase):
# try without a document content = get_router_status_entry_v3({'m': "8,9,10,11,12"}, content = True) - self._expect_invalid_attr(content, "microdescriptor_hashes") + self._expect_invalid_attr(content, "microdescriptor_hashes", expected_value = [])
# tries some invalid inputs test_values = (