commit 683b1ba479f2aff3c277e0ff53bdc8b1f81664af
Author: Damian Johnson <atagar(a)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 = (