tor-commits
Threads by month
- ----- 2025 -----
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
October 2012
- 20 participants
- 1288 discussions

13 Oct '12
commit be0c484de0be3cfa98a32c9ef5210b3e1d6eb6ee
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Aug 20 22:53:17 2012 -0700
Unit test for minimal RouterStatusEntry
As usual, starting the class unit testing by parsing a minimal example. Next
gonna go through field by field to exercise interesting use cases.
---
stem/descriptor/networkstatus.py | 16 ++++----
test/integ/descriptor/networkstatus.py | 8 ++--
test/unit/descriptor/networkstatus.py | 72 +++++++++++++++++++++++++++++--
3 files changed, 79 insertions(+), 17 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 5382c0f..96f10ce 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -441,7 +441,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
:var str nickname: **\*** router's nickname
:var str fingerprint: **\*** router's fingerprint
:var str digest: **\*** router's digest
- :var datetime publication: **\*** router's publication
+ :var datetime published: **\*** router's publication
:var str address: **\*** router's IP address
:var int or_port: **\*** router's ORPort
:var int dir_port: **\*** router's DirPort
@@ -479,7 +479,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
self.nickname = None
self.fingerprint = None
self.digest = None
- self.publication = None
+ self.published = None
self.address = None
self.or_port = None
self.dir_port = None
@@ -495,7 +495,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
self.exit_policy = None
self.microdescriptor_hashes = None
- self.unrecognized_lines = []
+ self._unrecognized_lines = []
self._parse(raw_contents, validate)
@@ -556,7 +556,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
try:
published = "%s %s" % (r_comp[3], r_comp[4])
- self.publication = datetime.datetime.strptime(published, "%Y-%m-%d %H:%M:%S")
+ self.published = datetime.datetime.strptime(published, "%Y-%m-%d %H:%M:%S")
except ValueError:
if validate:
raise ValueError("Publication time time wasn't parseable: %s" % line)
@@ -655,7 +655,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
self.microdescriptor_hashes.append((methods, hashes))
else:
- self.unrecognized_lines.append(line)
+ self._unrecognized_lines.append(line)
def get_unrecognized_lines(self):
"""
@@ -664,7 +664,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
:returns: list of unrecognized lines
"""
- return self.unrecognized_lines
+ return list(self._unrecognized_lines)
class MicrodescriptorConsensus(NetworkStatusDocument):
"""
@@ -706,7 +706,7 @@ class RouterMicrodescriptor(RouterStatusEntry):
:var str nickname: **\*** router's nickname
:var str fingerprint: **\*** router's fingerprint
- :var datetime publication: **\*** router's publication
+ :var datetime published: **\*** router's publication
:var str ip: **\*** router's IP address
:var int or_port: **\*** router's ORPort
:var int dir_port: **\*** router's DirPort
@@ -758,7 +758,7 @@ class RouterMicrodescriptor(RouterStatusEntry):
seen_keywords.add("r")
values = r.split(" ")
self.nickname, self.fingerprint = values[0], _decode_fingerprint(values[1], validate)
- self.publication = _strptime(" ".join((values[2], values[3])), validate)
+ self.published = _strptime(" ".join((values[2], values[3])), validate)
self.ip, self.or_port, self.dir_port = values[4], int(values[5]), int(values[6])
if self.dir_port == 0: self.dir_port = None
elif validate: raise ValueError("Invalid router descriptor: empty 'r' line")
diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py
index 4c626e9..c21b4a2 100644
--- a/test/integ/descriptor/networkstatus.py
+++ b/test/integ/descriptor/networkstatus.py
@@ -68,7 +68,7 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEquals("sumkledi", router.nickname)
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router.fingerprint)
self.assertEquals("8mCr8Sl7RF4ENU4jb0FZFA/3do8", router.digest)
- self.assertEquals(_strptime("2012-07-12 04:01:55"), router.publication)
+ self.assertEquals(_strptime("2012-07-12 04:01:55"), router.published)
self.assertEquals("178.218.213.229", router.address)
self.assertEquals(80, router.or_port)
self.assertEquals(None, router.dir_port)
@@ -113,7 +113,7 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEquals("sumkledi", router1.nickname)
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router1.fingerprint)
self.assertEquals("8mCr8Sl7RF4ENU4jb0FZFA/3do8", router1.digest)
- self.assertEquals(_strptime("2012-07-12 04:01:55"), router1.publication)
+ self.assertEquals(_strptime("2012-07-12 04:01:55"), router1.published)
self.assertEquals("178.218.213.229", router1.address)
self.assertEquals(80, router1.or_port)
self.assertEquals(None, router1.dir_port)
@@ -161,7 +161,7 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
self.assertEquals("sumkledi", router.nickname)
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router.fingerprint)
self.assertEquals("B5n4BiALAF8B5AqafxohyYiuj7E", router.digest)
- self.assertEquals(_strptime("2012-07-11 04:22:53"), router.publication)
+ self.assertEquals(_strptime("2012-07-11 04:22:53"), router.published)
self.assertEquals("178.218.213.229", router.address)
self.assertEquals(80, router.or_port)
self.assertEquals(None, router.dir_port)
@@ -198,7 +198,7 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
self.assertEquals("sumkledi", router1.nickname)
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router1.fingerprint)
self.assertEquals("B5n4BiALAF8B5AqafxohyYiuj7E", router1.digest)
- self.assertEquals(_strptime("2012-07-11 04:22:53"), router1.publication)
+ self.assertEquals(_strptime("2012-07-11 04:22:53"), router1.published)
self.assertEquals("178.218.213.229", router1.address)
self.assertEquals(80, router1.or_port)
self.assertEquals(None, router1.dir_port)
diff --git a/test/unit/descriptor/networkstatus.py b/test/unit/descriptor/networkstatus.py
index f14c2be..e6a8514 100644
--- a/test/unit/descriptor/networkstatus.py
+++ b/test/unit/descriptor/networkstatus.py
@@ -2,9 +2,44 @@
Unit tests for stem.descriptor.networkstatus.
"""
+import datetime
import unittest
-from stem.descriptor import networkstatus
+from stem.descriptor.networkstatus import Flag, RouterStatusEntry, _decode_fingerprint
+
+ROUTER_STATUS_ENTRY_ATTR = (
+ ("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
+ ("s", "Fast Named Running Stable Valid"),
+)
+
+def get_router_status_entry(attr = None, exclude = None):
+ """
+ Constructs a minimal router status entry with the given attributes.
+
+ :param dict attr: keyword/value mappings to be included in the entry
+ :param list exclude: mandatory keywords to exclude from the entry
+
+ :returns: str with customized router status entry content
+ """
+
+ descriptor_lines = []
+ if attr is None: attr = {}
+ if exclude is None: exclude = []
+ attr = dict(attr) # shallow copy since we're destructive
+
+ for keyword, value in ROUTER_STATUS_ENTRY_ATTR:
+ if keyword in exclude: continue
+ elif keyword in attr:
+ value = attr[keyword]
+ del attr[keyword]
+
+ descriptor_lines.append("%s %s" % (keyword, value))
+
+ # dump in any unused attributes
+ for attr_keyword, attr_value in attr.items():
+ descriptor_lines.append("%s %s" % (attr_keyword, attr_value))
+
+ return "\n".join(descriptor_lines)
class TestNetworkStatus(unittest.TestCase):
def test_fingerprint_decoding(self):
@@ -23,9 +58,36 @@ class TestNetworkStatus(unittest.TestCase):
}
for arg, expected in test_values.items():
- self.assertEqual(expected, networkstatus._decode_fingerprint(arg))
+ self.assertEqual(expected, _decode_fingerprint(arg, True))
+
+ # checks with some malformed inputs
+ for arg in ('', '20wYcb', '20wYcb' * 30):
+ self.assertRaises(ValueError, _decode_fingerprint, arg, True)
+ self.assertEqual(None, _decode_fingerprint(arg, False))
+
+ def test_rse_minimal(self):
+ """
+ Parses a minimal router status entry.
+ """
+
+ entry = RouterStatusEntry(get_router_status_entry(), None)
- self.assertRaises(ValueError, networkstatus._decode_fingerprint, '')
- self.assertRaises(ValueError, networkstatus._decode_fingerprint, '20wYcb')
- self.assertRaises(ValueError, networkstatus._decode_fingerprint, '20wYcb' * 30)
+ expected_flags = set([Flag.FAST, Flag.NAMED, Flag.RUNNING, Flag.STABLE, Flag.VALID])
+ self.assertEqual(None, entry.document)
+ self.assertEqual("caerSidi", entry.nickname)
+ self.assertEqual("A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB", entry.fingerprint)
+ self.assertEqual("oQZFLYe9e4A7bOkWKR7TaNxb0JE", entry.digest)
+ self.assertEqual(datetime.datetime(2012, 8, 6, 11, 19, 31), entry.published)
+ self.assertEqual("71.35.150.29", entry.address)
+ self.assertEqual(9001, entry.or_port)
+ self.assertEqual(None, entry.dir_port)
+ self.assertEqual(expected_flags, set(entry.flags))
+ self.assertEqual(None, entry.version_line)
+ self.assertEqual(None, entry.version)
+ self.assertEqual(None, entry.bandwidth)
+ 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.get_unrecognized_lines())
1
0
commit a33297c8f65552c1ef721277e056a89f2b2727b7
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Aug 20 22:20:44 2012 -0700
Rewriting RouterStatusEntry parser
The networkstatus module has spotty error checking, for instance...
* the unrecognized_lines attribute is never initialized, so anything touching
it will trigger a NameError
* missing values on an 'r' line will result in an IndexError, and extra values
are ignored
* malformed nicknames, ip addresses, and ports go undetected
* non-integer ports or bandwidth/measured values give an 'invalid literal'
error, which isn't terribly helpful
* extra entries on a 'w' line inappropriately cause an error
* malformed exit policies can cause an error when validation is disabled
* we error if there's new keywords (the spec doesn't seem to say they're
disallowed...)
From the look of this code the 'validate' flag seems to have been
misinterpreted as meaning "raise an exception if we run into something
unexpected, like a new keyword or flag". This is wrong. Stem should *never*
error when processing spec conformant content. Rather, the 'validate' flag
means "raise an exception when we parse something that violates the spec". When
that flag is False the parser should *never* raise an exception, making a
best-effort attempt to parse even malformed content.
I'm a bit sad to see that there are absolutely no unit tests for this module.
That was the bulk of the testing for the server_descriptor and
extrainfo_descriptor modules. Guess I'll write that next.
---
stem/descriptor/networkstatus.py | 339 +++++++++++++++++++++-----------
test/integ/descriptor/networkstatus.py | 32 ++--
2 files changed, 242 insertions(+), 129 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index b7a1b2f..5382c0f 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -330,8 +330,8 @@ class DirectoryAuthority(stem.descriptor.Descriptor):
:var str fingerprint: uppercase hex fingerprint of the authority's identity key
:var str address: hostname
:var str ip: current IP address
- :var int dirport: current directory port
- :var int orport: current orport
+ :var int dir_port: current directory port
+ :var int or_port: current orport
:var str contact: directory authority's contact information
:var str legacy_dir_key: **^** fingerprint of and obsolete identity key
:var :class:`stem.descriptor.KeyCertificate` key_certificate: **^** directory authority's current key certificate
@@ -355,14 +355,14 @@ class DirectoryAuthority(stem.descriptor.Descriptor):
super(DirectoryAuthority, self).__init__(raw_content)
self.nickname, self.fingerprint, self.address, self.ip = None, None, None, None
- self.dirport, self.orport, self.legacy_dir_key = None, None, None
+ self.dir_port, self.or_port, self.legacy_dir_key = None, None, None
self.key_certificate, self.contact, self.vote_digest = None, None, None
content = StringIO(raw_content)
dir_source = _read_keyword_line("dir-source", content, validate)
- self.nickname, self.fingerprint, self.address, self.ip, self.dirport, self.orport = dir_source.split(" ")
- self.dirport = int(self.dirport)
- self.orport = int(self.orport)
+ self.nickname, self.fingerprint, self.address, self.ip, self.dir_port, self.or_port = dir_source.split(" ")
+ self.dir_port = int(self.dir_port)
+ self.or_port = int(self.or_port)
self.contact = _read_keyword_line("contact", content, validate)
if vote:
@@ -442,33 +442,32 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
:var str fingerprint: **\*** router's fingerprint
:var str digest: **\*** router's digest
:var datetime publication: **\*** router's publication
- :var str ip: **\*** router's IP address
- :var int orport: **\*** router's ORPort
- :var int dirport: **\*** router's DirPort
-
+ :var str address: **\*** router's IP address
+ :var int or_port: **\*** router's ORPort
+ :var int dir_port: **\*** router's DirPort
:var list flags: **\*** list of status flags
- :var :class:`stem.version.Version`,str version: Version of the Tor protocol this router is running
+ :var stem.version.Version version: parsed version of tor, this is None if the relay's using a new versioning scheme
+ :var str version_line: versioning information reported by the relay
- :var int bandwidth: router's claimed bandwidth
- :var int measured_bandwidth: router's measured bandwidth
+ :var int bandwidth: bandwidth claimed by the relay (in kb/s)
+ :var int measured: bandwith measured to be available by the relay
+ :var list unrecognized_bandwidth_entries: **\*** bandwidth weighting information that isn't yet recognized
- :var :class:`stem.exit_policy.MicrodescriptorExitPolicy` exit_policy: router's exit policy
+ :var stem.exit_policy.MicrodescriptorExitPolicy exit_policy: router's exit policy
- :var str microdescriptor_hashes: a list of two-tuples with a list of consensus methods(int) that may produce the digest and a dict with algorithm(str) => digest(str) mappings. algorithm is the hashing algorithm (usually "sha256") that is used to produce digest (the base64 encoding of the hash of the router's microdescriptor with trailing =s omitted).
+ :var list microdescriptor_hashes: tuples of two values, the list of consensus methods for generting 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
- | exit_policy appears only in votes
+ **\*** 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, raw_contents, document, validate = True):
"""
- Parse a router descriptor in a v3 network status document and provide a new
- RouterStatusEntry object.
+ Parse a router descriptor in a v3 network status document.
:param str raw_content: router descriptor content to be parsed
:param NetworkStatusDocument document: document this descriptor came from
- :param bool validate: whether the router descriptor should be validated
+ :param bool validate: checks the validity of the content if True, skips these checks otherwise
:raises: ValueError if the descriptor data is invalid
"""
@@ -481,116 +480,188 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
self.fingerprint = None
self.digest = None
self.publication = None
- self.ip = None
- self.orport = None
- self.dirport = None
+ self.address = None
+ self.or_port = None
+ self.dir_port = None
- self.flags = []
+ self.flags = None
+ self.version_line = None
self.version = None
self.bandwidth = None
- self.measured_bandwidth = None
+ self.measured = None
+ self.unrecognized_bandwidth_entries = []
self.exit_policy = None
-
- self.microdescriptor_hashes = []
+ self.microdescriptor_hashes = None
+ self.unrecognized_lines = []
self._parse(raw_contents, validate)
- def _parse(self, raw_content, validate):
+ def _parse(self, content, validate):
"""
- :param dict raw_content: iptor contents to be applied
- :param bool validate: checks the validity of descriptor content if True
+ Parses the given content and applies the attributes.
- :raises: ValueError if an error occures in validation
+ :param str content: descriptor content
+ :param bool validate: checks validity if True
+
+ :raises: ValueError if a validity check fails
"""
- vote = self.document.vote_status == "vote"
- content = StringIO(raw_content)
- seen_keywords = set()
- peek_check_kw = lambda keyword: keyword == _peek_keyword(content)
+ entries = _get_entries(content, validate, 'r')
- r = _read_keyword_line("r", content, validate)
- # r mauer BD7xbfsCFku3+tgybEZsg8Yjhvw itcuKQ6PuPLJ7m/Oi928WjO2j8g 2012-06-22 13:19:32 80.101.105.103 9001 0
- # "r" SP nickname SP identity SP digest SP publication SP IP SP ORPort SP DirPort NL
- if r:
- seen_keywords.add("r")
- values = r.split(" ")
- self.nickname, self.fingerprint, self.digest = values[0], _decode_fingerprint(values[1]), values[2]
- self.publication = _strptime(" ".join((values[3], values[4])), validate)
- self.ip, self.orport, self.dirport = values[5], int(values[6]), int(values[7])
- if self.dirport == 0: self.dirport = None
- elif validate: raise ValueError("Invalid router descriptor: empty 'r' line")
+ # check that we have mandatory fields
+ if validate:
+ for keyword in ('r', 's'):
+ if not keyword in entries:
+ raise ValueError("Router status entries must have a '%s' line:\n%s" % (keyword, content))
- while _peek_line(content):
- if peek_check_kw("s"):
- if "s" in seen_keywords: raise ValueError("Invalid router descriptor: 's' line appears twice")
- line = _read_keyword_line("s", content, validate)
- if not line: continue
- seen_keywords.add("s")
- # s Named Running Stable Valid
- #A series of space-separated status flags, in *lexical order*
- self.flags = line.split(" ")
+ for keyword, values in entries.items():
+ value = values[0]
+ line = "%s %s" % (keyword, value)
- elif peek_check_kw("v"):
- if "v" in seen_keywords: raise ValueError("Invalid router descriptor: 'v' line appears twice")
- line = _read_keyword_line("v", content, validate, True)
- seen_keywords.add("v")
- # v Tor 0.2.2.35
- if line:
- if line.startswith("Tor "):
- self.version = stem.version.Version(line[4:])
- else:
- self.version = line
- elif validate: raise ValueError("Invalid router descriptor: empty 'v' line" )
+ # most attributes can only appear at most once
+ if validate and len(values) > 1 and keyword in ('r', 's', 'v', 'w', 'p'):
+ raise ValueError("Router status entries can only have a single '%s' line, got %i:\n%s" % (key, len(values), content))
- elif peek_check_kw("w"):
- if "w" in seen_keywords: raise ValueError("Invalid router descriptor: 'w' line appears twice")
- w = _read_keyword_line("w", content, validate, True)
- # "w" SP "Bandwidth=" INT [SP "Measured=" INT] NL
- seen_keywords.add("w")
- if w:
- values = w.split(" ")
- if len(values) <= 2 and len(values) > 0:
- key, value = values[0].split("=")
- if key == "Bandwidth": self.bandwidth = int(value)
- elif validate: raise ValueError("Router descriptor contains invalid 'w' line: expected Bandwidth, read " + key)
+ if keyword == 'r':
+ # "r" nickname identity digest publication IP ORPort DirPort
+ # r mauer BD7xbfsCFku3+tgybEZsg8Yjhvw itcuKQ6PuPLJ7m/Oi928WjO2j8g 2012-06-22 13:19:32 80.101.105.103 9001 0
+
+ r_comp = value.split(" ")
+
+ if len(r_comp) < 5:
+ if not validate: continue
+ raise ValueError("Router status entry's 'r' line line must have eight values: %s" % line)
+
+ if validate:
+ if not stem.util.tor_tools.is_valid_nickname(r_comp[0]):
+ raise ValueError("Router status entry's nickname isn't valid: %s" % r_comp[0])
+ elif not stem.util.connection.is_valid_ip_address(r_comp[5]):
+ raise ValueError("Router status entry's address isn't a valid IPv4 address: %s" % r_comp[5])
+ elif not stem.util.connection.is_valid_port(r_comp[6]):
+ raise ValueError("Router status entry's ORPort is invalid: %s" % r_comp[6])
+ elif not stem.util.connection.is_valid_port(r_comp[7], allow_zero = True):
+ raise ValueError("Router status entry's DirPort is invalid: %s" % r_comp[7])
+ elif not (r_comp[6].isdigit() and r_comp[7].isdigit()):
+ continue
+
+ self.nickname = r_comp[0]
+ self.fingerprint = _decode_fingerprint(r_comp[1], validate)
+ self.digest = r_comp[2]
+ self.address = r_comp[5]
+ self.or_port = int(r_comp[6])
+ self.dir_port = None if r_comp[7] == '0' else int(r_comp[7])
+
+ try:
+ published = "%s %s" % (r_comp[3], r_comp[4])
+ self.publication = datetime.datetime.strptime(published, "%Y-%m-%d %H:%M:%S")
+ except ValueError:
+ if validate:
+ raise ValueError("Publication time time wasn't parseable: %s" % line)
+ elif keyword == 's':
+ # "s" Flags
+ # s Named Running Stable Valid
+
+ self.flags = value.split(" ")
+ elif keyword == 'v':
+ # "v" version
+ # v Tor 0.2.2.35
+ #
+ # The spec says that if this starts with "Tor " then what follows is a
+ # tor version. If not then it has "upgraded to a more sophisticated
+ # protocol versioning system".
+
+ self.version_line = value
+
+ if value.startswith("Tor "):
+ try:
+ self.version = stem.version.Version(value[4:])
+ except ValueError, exc:
+ if validate:
+ raise ValueError("Router status entry has a malformed tor version (%s): %s" % (exc, line))
+ elif keyword == 'w':
+ # "w" "Bandwidth=" INT ["Measured=" INT]
+ # w Bandwidth=7980
+
+ w_comp = value.split(" ")
+
+ if len(w_comp) < 1:
+ if not validate: continue
+ raise ValueError("Router status entry's 'w' line is blank: %s" % line)
+ elif not w_comp[0].startswith("Bandwidth="):
+ if not validate: continue
+ raise ValueError("Router status entry's 'w' line needs to start with a 'Bandwidth=' entry: %s" % line)
+
+ for w_entry in w_comp:
+ w_key, w_value = w_entry.split('=', 1)
+
+ if w_key == "Bandwidth":
+ if not w_value.isdigit():
+ if not validate: continue
+ raise ValueError("Router status entry's 'Bandwidth=' entry needs to have a numeric value: %s" % line)
- if len(values) == 2:
- key, value = values[1].split("=")
- if key == "Measured": self.measured_bandwidth = int(value)
- elif validate: raise ValueError("Router descriptor contains invalid 'w' line: expected Measured, read " + key)
- elif validate: raise ValueError("Router descriptor contains invalid 'w' line")
- elif validate: raise ValueError("Router descriptor contains empty 'w' line")
-
- elif peek_check_kw("p"):
- if "p" in seen_keywords: raise ValueError("Invalid router descriptor: 'p' line appears twice")
- p = _read_keyword_line("p", content, validate, True)
- seen_keywords.add("p")
- # "p" SP ("accept" / "reject") SP PortList NL
- if p:
- self.exit_policy = stem.exit_policy.MicrodescriptorExitPolicy(p)
-
- elif vote and peek_check_kw("m"):
- # microdescriptor hashes
- m = _read_keyword_line("m", content, validate, True)
- methods, digests = m.split(" ", 1)
- method_list = methods.split(",")
- digest_dict = [digest.split("=", 1) for digest in digests.split(" ")]
- self.microdescriptor_hashes.append((method_list, digest_dict))
-
- elif validate:
- raise ValueError("Router descriptor contains unrecognized trailing lines: %s" % content.readline())
-
+ self.bandwidth = int(w_value)
+ elif w_key == "Measured":
+ if not w_value.isdigit():
+ if not validate: continue
+ raise ValueError("Router status entry's 'Measured=' entry needs to have a numeric value: %s" % line)
+
+ self.measured = int(w_value)
+ else:
+ self.unrecognized_bandwidth_entries.append(w_entry)
+ elif keyword == 'p':
+ # "p" ("accept" / "reject") PortList
+ # p reject 1-65535
+ # p accept 80,110,143,443,993,995,6660-6669,6697,7000-7001
+
+ try:
+ self.exit_policy = stem.exit_policy.MicrodescriptorExitPolicy(value)
+ except ValueError, exc:
+ if not validate: continue
+ raise ValueError("Router status entry's exit policy is malformed (%s): %s" % (exc, line))
+ elif keyword == 'm':
+ # "m" methods 1*(algorithm "=" digest)
+ # m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs
+
+ m_comp = value.split(" ")
+
+ if self.document.vote_status != "vote":
+ if not validate: continue
+ raise ValueError("Router status entry's 'm' line should only appear in votes (appeared in a %s): %s" % (self.document.vote_status, line))
+ elif len(m_comp) < 1:
+ if not validate: continue
+ raise ValueError("Router status entry's 'm' line needs to start with a series of methods: %s" % line)
+
+ try:
+ methods = [int(entry) for entry in m_comp[0].split(",")]
+ except ValueError:
+ if not validate: continue
+ raise ValueError("Router status entry's microdescriptor methods should be a series of comma separated integers: %s" % line)
+
+ hashes = {}
+
+ for entry in m_comp[1:]:
+ if not '=' in entry:
+ if not validate: continue
+ raise ValueError("Router status entry's can only have a series of 'algorithm=digest' mappings after the methods: %s" % line)
+
+ hash_name, digest = entry.split('=', 1)
+ hashes[hash_name] = digest
+
+ if self.microdescriptor_hashes is None:
+ self.microdescriptor_hashes = []
+
+ self.microdescriptor_hashes.append((methods, hashes))
else:
- self.unrecognized_lines.append(content.readline()) # ignore unrecognized lines if we aren't validating
+ self.unrecognized_lines.append(line)
def get_unrecognized_lines(self):
"""
- Returns any unrecognized lines.
+ Provides any unrecognized lines.
- :returns: a list of unrecognized lines
+ :returns: list of unrecognized lines
"""
return self.unrecognized_lines
@@ -637,8 +708,8 @@ class RouterMicrodescriptor(RouterStatusEntry):
:var str fingerprint: **\*** router's fingerprint
:var datetime publication: **\*** router's publication
:var str ip: **\*** router's IP address
- :var int orport: **\*** router's ORPort
- :var int dirport: **\*** router's DirPort
+ :var int or_port: **\*** router's ORPort
+ :var int dir_port: **\*** router's DirPort
:var list flags: **\*** list of status flags
@@ -686,10 +757,10 @@ class RouterMicrodescriptor(RouterStatusEntry):
if r:
seen_keywords.add("r")
values = r.split(" ")
- self.nickname, self.fingerprint = values[0], _decode_fingerprint(values[1])
+ self.nickname, self.fingerprint = values[0], _decode_fingerprint(values[1], validate)
self.publication = _strptime(" ".join((values[2], values[3])), validate)
- self.ip, self.orport, self.dirport = values[4], int(values[5]), int(values[6])
- if self.dirport == 0: self.dirport = None
+ self.ip, self.or_port, self.dir_port = values[4], int(values[5]), int(values[6])
+ if self.dir_port == 0: self.dir_port = None
elif validate: raise ValueError("Invalid router descriptor: empty 'r' line")
while _peek_line(content):
@@ -752,7 +823,45 @@ class RouterMicrodescriptor(RouterStatusEntry):
return self.unrecognized_lines
-def _decode_fingerprint(identity):
+def _get_entries(content, validate, expected_first_keyword = None):
+ """
+ Provides the {keyword => [values...]} mappings for the given content.
+
+ :param str content: descriptor content
+ :param bool validate: checks validity if True
+ :param str expected_first_keyword: validates that this is the first keyword
+
+ :returns: dict with the mapping of keywords to their values
+
+ :raises: ValueError if a validity check fails
+ """
+
+ entries = {}
+
+ for line in content.split("\n"):
+ # empty lines are allowed
+ if not line: continue
+
+ line_match = stem.descriptor.KEYWORD_LINE.match(line)
+
+ if not line_match:
+ if not validate: continue
+ raise ValueError("Line contains invalid characters: %s" % line)
+
+ keyword, value = line_match.groups()
+ if value is None: value = ''
+
+ if expected_first_keyword != None:
+ if validate and expected_first_keyword != keyword:
+ raise ValueError("Expected to start with a '%s' line:\n%s" % (expected_first_keyword, content))
+
+ expected_first_keyword = None
+
+ entries.setdefault(keyword, []).append(value)
+
+ return entries
+
+def _decode_fingerprint(identity, validate):
"""
Decodes the 'identity' value found in consensuses into the more common hex
encoding of the relay's fingerprint. For example...
@@ -763,6 +872,7 @@ def _decode_fingerprint(identity):
'A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB'
:param str identity: encoded fingerprint from the consensus
+ :param bool validate: checks validity if True
:returns: str with the uppercase hex encoding of the relay's fingerprint
@@ -788,7 +898,10 @@ def _decode_fingerprint(identity):
fingerprint += hex(ord(char))[2:].zfill(2).upper()
if not stem.util.tor_tools.is_valid_fingerprint(fingerprint):
- raise ValueError("Decoded '%s' to be '%s', which isn't a valid fingerprint" % (identity, fingerprint))
+ if validate:
+ raise ValueError("Decoded '%s' to be '%s', which isn't a valid fingerprint" % (identity, fingerprint))
+ else:
+ return None
return fingerprint
diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py
index 040a710..4c626e9 100644
--- a/test/integ/descriptor/networkstatus.py
+++ b/test/integ/descriptor/networkstatus.py
@@ -69,9 +69,9 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router.fingerprint)
self.assertEquals("8mCr8Sl7RF4ENU4jb0FZFA/3do8", router.digest)
self.assertEquals(_strptime("2012-07-12 04:01:55"), router.publication)
- self.assertEquals("178.218.213.229", router.ip)
- self.assertEquals(80, router.orport)
- self.assertEquals(None, router.dirport)
+ self.assertEquals("178.218.213.229", router.address)
+ self.assertEquals(80, router.or_port)
+ self.assertEquals(None, router.dir_port)
def test_consensus(self):
"""
@@ -114,9 +114,9 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router1.fingerprint)
self.assertEquals("8mCr8Sl7RF4ENU4jb0FZFA/3do8", router1.digest)
self.assertEquals(_strptime("2012-07-12 04:01:55"), router1.publication)
- self.assertEquals("178.218.213.229", router1.ip)
- self.assertEquals(80, router1.orport)
- self.assertEquals(None, router1.dirport)
+ self.assertEquals("178.218.213.229", router1.address)
+ self.assertEquals(80, router1.or_port)
+ self.assertEquals(None, router1.dir_port)
self.assertEquals(set(["Exit", "Fast", "Named", "Running", "Valid"]), set(router1.flags))
self.assertEquals(8, len(desc.directory_authorities))
@@ -124,8 +124,8 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEquals("14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4", desc.directory_authorities[0].fingerprint)
self.assertEquals("86.59.21.38", desc.directory_authorities[0].address)
self.assertEquals("86.59.21.38", desc.directory_authorities[0].ip)
- self.assertEquals(80, desc.directory_authorities[0].dirport)
- self.assertEquals(443, desc.directory_authorities[0].orport)
+ self.assertEquals(80, desc.directory_authorities[0].dir_port)
+ self.assertEquals(443, desc.directory_authorities[0].or_port)
self.assertEquals("Peter Palfrader", desc.directory_authorities[0].contact)
self.assertEquals(None, desc.directory_authorities[0].legacy_dir_key)
self.assertEquals(None, desc.directory_authorities[0].key_certificate)
@@ -162,9 +162,9 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router.fingerprint)
self.assertEquals("B5n4BiALAF8B5AqafxohyYiuj7E", router.digest)
self.assertEquals(_strptime("2012-07-11 04:22:53"), router.publication)
- self.assertEquals("178.218.213.229", router.ip)
- self.assertEquals(80, router.orport)
- self.assertEquals(None, router.dirport)
+ self.assertEquals("178.218.213.229", router.address)
+ self.assertEquals(80, router.or_port)
+ self.assertEquals(None, router.dir_port)
def test_vote(self):
"""
@@ -199,17 +199,17 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
self.assertEquals("0013D22389CD50D0B784A3E4061CB31E8CE8CEB5", router1.fingerprint)
self.assertEquals("B5n4BiALAF8B5AqafxohyYiuj7E", router1.digest)
self.assertEquals(_strptime("2012-07-11 04:22:53"), router1.publication)
- self.assertEquals("178.218.213.229", router1.ip)
- self.assertEquals(80, router1.orport)
- self.assertEquals(None, router1.dirport)
+ self.assertEquals("178.218.213.229", router1.address)
+ self.assertEquals(80, router1.or_port)
+ self.assertEquals(None, router1.dir_port)
self.assertEquals(1, len(desc.directory_authorities))
self.assertEquals("turtles", desc.directory_authorities[0].nickname)
self.assertEquals("27B6B5996C426270A5C95488AA5BCEB6BCC86956", desc.directory_authorities[0].fingerprint)
self.assertEquals("76.73.17.194", desc.directory_authorities[0].address)
self.assertEquals("76.73.17.194", desc.directory_authorities[0].ip)
- self.assertEquals(9030, desc.directory_authorities[0].dirport)
- self.assertEquals(9090, desc.directory_authorities[0].orport)
+ self.assertEquals(9030, desc.directory_authorities[0].dir_port)
+ self.assertEquals(9090, desc.directory_authorities[0].or_port)
self.assertEquals("Mike Perry <email>", desc.directory_authorities[0].contact)
self.assertEquals(None, desc.directory_authorities[0].legacy_dir_key)
1
0

13 Oct '12
commit d30a628e5da06699d8533f92c4b1c496510213b8
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Sep 2 17:06:20 2012 -0700
Checking for mandatory and disallowed fields
There's several restrictions on a valid network status document, some of which
are which fields it does and does not contain. Validating that manitory fields
apprear, and that fields which shouldn't appear don't.
This also drops my _get_entries() helper function in favor of
_get_descriptor_components() since we're now dealing with documents which can
contain key blocks. There wasn't really enough of an advantage to
_get_entries() to justify the duplicate code anyway.
Also fixing the _get_descriptor_components() I wrote so it return footer
content in the header when the content doens't contain any 'r' lines.
This currently keeps the old parser (renamed to "_parse_old()") since I haven't
addressed any of the actual parsing yet.
---
stem/descriptor/__init__.py | 22 +++--
stem/descriptor/networkstatus.py | 154 ++++++++++++++++++++------------
test/integ/descriptor/networkstatus.py | 2 -
3 files changed, 112 insertions(+), 66 deletions(-)
diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
index fb81237..688e1b6 100644
--- a/stem/descriptor/__init__.py
+++ b/stem/descriptor/__init__.py
@@ -25,6 +25,7 @@ __all__ = [
import os
import re
import datetime
+import collections
KEYWORD_CHAR = "a-zA-Z0-9-"
WHITESPACE = " \t"
@@ -335,7 +336,7 @@ def _get_pseudo_pgp_block(remaining_contents):
else:
return None
-def _get_descriptor_components(raw_contents, validate, extra_keywords):
+def _get_descriptor_components(raw_contents, validate, extra_keywords = ()):
"""
Initial breakup of the server descriptor contents to make parsing easier.
@@ -356,13 +357,13 @@ def _get_descriptor_components(raw_contents, validate, extra_keywords):
:returns:
tuple with the following attributes...
- * **entries (dict)** - keyword => (value, pgp key) entries
+ * **entries (collections.OrderedDict)** - keyword => (value, pgp key) entries
* **first_keyword (str)** - keyword of the first line
* **last_keyword (str)** - keyword of the last line
* **extra_entries (list)** - lines containing entries matching extra_keywords
"""
- entries = {}
+ entries = collections.OrderedDict()
first_keyword = None
last_keyword = None
extra_entries = [] # entries with a keyword in extra_keywords
@@ -371,8 +372,15 @@ def _get_descriptor_components(raw_contents, validate, extra_keywords):
while remaining_lines:
line = remaining_lines.pop(0)
- # last line can be empty
- if not line and not remaining_lines: continue
+ # V2 network status documents explicitely can contain blank lines...
+ #
+ # "Implementations MAY insert blank lines for clarity between sections;
+ # these blank lines are ignored."
+ #
+ # ... and server descriptors end with an extra newline. But other documents
+ # don't say how blank lines should be handled so globally ignoring them.
+
+ if not line: continue
# Some lines have an 'opt ' for backward compatability. They should be
# ignored. This prefix is being removed in...
@@ -400,10 +408,8 @@ def _get_descriptor_components(raw_contents, validate, extra_keywords):
if keyword in extra_keywords:
extra_entries.append("%s %s" % (keyword, value))
- elif keyword in entries:
- entries[keyword].append((value, block_contents))
else:
- entries[keyword] = [(value, block_contents)]
+ entries.setdefault(keyword, []).append((value, block_contents))
return entries, first_keyword, last_keyword, extra_entries
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 021ac4c..168c768 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -76,6 +76,34 @@ Flag = stem.util.enum.Enum(
("VALID", "Valid"),
)
+# Network status document are either a 'vote' or 'consensus', with different
+# mandatory fields for each. Both though require that their fields appear in a
+# specific order. This is an ordered listing of the following...
+#
+# (field, in_votes, in_consensus, is_mandatory)
+
+HEADER_STATUS_DOCUMENT_FIELDS = (
+ ("network-status-version", True, True, True),
+ ("vote-status", True, True, True),
+ ("consensus-methods", True, False, False),
+ ("consensus-method", False, True, False),
+ ("published", True, False, True),
+ ("valid-after", True, True, True),
+ ("fresh-until", True, True, True),
+ ("valid-until", True, True, True),
+ ("voting-delay", True, True, True),
+ ("client-versions", True, True, False),
+ ("server-versions", True, True, False),
+ ("known-flags", True, True, True),
+ ("params", True, True, False),
+)
+
+FOOTER_STATUS_DOCUMENT_FIELDS = (
+ ("directory-footer", True, True, True),
+ ("bandwidths-weights", False, True, False),
+ ("directory-signature", True, True, True),
+)
+
def parse_file(document_file, validate = True, is_microdescriptor = False):
"""
Parses a network status and iterates over the RouterStatusEntry or
@@ -94,7 +122,7 @@ def parse_file(document_file, validate = True, is_microdescriptor = False):
"""
header, footer, routers_end = _get_document_content(document_file, validate)
- document_data = "".join(header + footer)
+ document_data = header + footer
if not is_microdescriptor:
document = NetworkStatusDocument(document_data, validate)
@@ -110,7 +138,7 @@ def _get_document_content(document_file, validate):
"""
Network status documents consist of three sections: header, router entries,
and the footer. This provides back a tuple with the following...
- (header_lines, footer_lines, routers_end)
+ (header, footer, routers_end)
This leaves the document_file at the start of the router entries.
@@ -126,7 +154,7 @@ def _get_document_content(document_file, validate):
# parse until the first router record
- header = _read_until_keywords("r", document_file)
+ header = _read_until_keywords(("r", "directory-footer", "directory-signature"), document_file)
routers_start = document_file.tell()
# figure out the network status version
@@ -134,12 +162,12 @@ def _get_document_content(document_file, validate):
# TODO: we should pick either 'directory-footer' or 'directory-signature'
# based on the header's network-status-version
- _read_until_keywords(["directory-footer", "directory-signature"], document_file, skip = True)
+ _read_until_keywords(("directory-footer", "directory-signature"), document_file, skip = True)
routers_end = document_file.tell()
footer = document_file.readlines()
document_file.seek(routers_start)
- return (header, footer, routers_end)
+ return ("".join(header), "".join(footer), routers_end)
def _get_routers(document_file, validate, document, end_position, router_type):
"""
@@ -166,14 +194,11 @@ def _get_routers(document_file, validate, document, end_position, router_type):
class NetworkStatusDocument(stem.descriptor.Descriptor):
"""
- A v3 network status document.
-
- This could be a v3 consensus or vote document.
+ Version 3 network status document. This could be either a vote or consensus.
:var tuple routers: RouterStatusEntry contained in the document
- :var bool validated: **\*** whether the document is validated
- :var str network_status_version: **\*** a document format version. For v3 documents this is "3"
+ :var int network_status_version: **\*** document version
:var str vote_status: **\*** status of the vote (is either "vote" or "consensus")
:var list consensus_methods: **^** A list of supported consensus generation methods (integers)
:var datetime published: **^** time when the document was published
@@ -210,7 +235,6 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.directory_authorities = []
self.directory_signatures = []
- self.validated = validate
self.network_status_version = None
self.vote_status = None
@@ -231,8 +255,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
document_file = StringIO(raw_content)
header, footer, routers_end = _get_document_content(document_file, validate)
- document_content = "".join(header + footer)
- self._parse(document_content)
+ self._parse(header, footer, validate)
+ self._parse_old(header + footer, validate)
if document_file.tell() < routers_end:
self.routers = tuple(_get_routers(document_file, validate, self, routers_end, self._get_router_type()))
@@ -254,10 +278,30 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
return self.unrecognized_lines
- def _parse(self, raw_content):
+ def _parse(self, header, footer, validate):
+ """
+ Parses the given content and applies the attributes.
+
+ :param str header_content: content of the descriptor header
+ :param str footer_content: content of the descriptor footer
+ :param bool validate: checks validity if True
+
+ :raises: ValueError if a validity check fails
+ """
+
+ header_entries = stem.descriptor._get_descriptor_components(header, validate)[0]
+ footer_entries = stem.descriptor._get_descriptor_components(footer, validate)[0]
+
+ if validate:
+ if not 'vote-status' in header_entries:
+ raise ValueError("Network status documents must have a 'vote-status' line to say if they're a vote or consensus")
+
+ is_consensus = header_entries['vote-status'][0][0] == "consensus"
+ self._check_for_missing_and_disallowed_fields(is_consensus, header_entries, footer_entries)
+
+ def _parse_old(self, raw_content, validate):
# preamble
content = StringIO(raw_content)
- validate = self.validated
read_keyword_line = lambda keyword, optional = False: setattr(self, keyword.replace("-", "_"), _read_keyword_line(keyword, content, validate, optional))
map(read_keyword_line, ["network-status-version", "vote-status"])
@@ -320,6 +364,40 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.unrecognized_lines = content.read()
if validate and self.unrecognized_lines: raise ValueError("Unrecognized trailing data")
+
+ def _check_for_missing_and_disallowed_fields(self, is_consensus, header_entries, footer_entries):
+ """
+ Checks that we have mandatory fields for our type, and that we don't have
+ any fields exclusive to the other (ie, no vote-only fields appear in a
+ consensus or vice versa).
+
+ :param bool is_consensus: true if we're a conensus and false if we're a vote
+ :param dict header_entries: ordered keyword/value mappings of the header
+ :param dict footer_entries: ordered keyword/value mappings of the footer
+
+ :raises: ValueError if we're missing mandatory fields or have fiels we shouldn't
+ """
+
+ is_vote = not is_consensus # aliasing inverse for readability
+ missing_fields, disallowed_fields = [], []
+
+ for entries, fields in ((header_entries, HEADER_STATUS_DOCUMENT_FIELDS),\
+ (footer_entries, FOOTER_STATUS_DOCUMENT_FIELDS)):
+ for field, in_votes, in_consensus, mandatory in fields:
+ if mandatory and ((is_consensus and in_consensus) or (is_vote and in_votes)):
+ # mandatory field, check that we have it
+ if not field in entries.keys():
+ missing_fields.append(field)
+ elif (is_consensus and not in_consensus) or (is_vote and not in_votes):
+ # field we shouldn't have, check that we don't
+ if field in entries.keys():
+ disallowed_fields.append(field)
+
+ if missing_fields:
+ raise ValueError("Network status document is missing mandatory field: %s" % ', '.join(missing_fields))
+
+ if disallowed_fields:
+ raise ValueError("Network status document has fields that shouldn't appear in this document type: %s" % ', '.join(disallowed_fields))
class DirectoryAuthority(stem.descriptor.Descriptor):
"""
@@ -509,7 +587,10 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
:raises: ValueError if a validity check fails
"""
- entries = _get_entries(content, validate, 'r')
+ entries, first_keyword, _, _ = stem.descriptor._get_descriptor_components(content, validate)
+
+ if validate and first_keyword != 'r':
+ raise ValueError("Router status entries are expected to start with a 'r' line:\n%s" % (content))
# check that we have mandatory fields
if validate:
@@ -518,7 +599,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
raise ValueError("Router status entries must have a '%s' line:\n%s" % (keyword, content))
for keyword, values in entries.items():
- value = values[0]
+ value, block_contents = values[0]
line = "%s %s" % (keyword, value)
# most attributes can only appear at most once
@@ -685,7 +766,6 @@ class MicrodescriptorConsensus(NetworkStatusDocument):
"""
A v3 microdescriptor consensus.
- :var bool validated: **\*** whether the document is validated
:var str network_status_version: **\*** a document format version. For v3 microdescriptor consensuses this is "3 microdesc"
:var str vote_status: **\*** status of the vote (is "consensus")
:var int consensus_method: **~** consensus method used to generate a consensus
@@ -838,44 +918,6 @@ class RouterMicrodescriptor(RouterStatusEntry):
return self.unrecognized_lines
-def _get_entries(content, validate, expected_first_keyword = None):
- """
- Provides the {keyword => [values...]} mappings for the given content.
-
- :param str content: descriptor content
- :param bool validate: checks validity if True
- :param str expected_first_keyword: validates that this is the first keyword
-
- :returns: dict with the mapping of keywords to their values
-
- :raises: ValueError if a validity check fails
- """
-
- entries = {}
-
- for line in content.split("\n"):
- # empty lines are allowed
- if not line: continue
-
- line_match = stem.descriptor.KEYWORD_LINE.match(line)
-
- if not line_match:
- if not validate: continue
- raise ValueError("Line contains invalid characters: %s" % line)
-
- keyword, value = line_match.groups()
- if value is None: value = ''
-
- if expected_first_keyword != None:
- if validate and expected_first_keyword != keyword:
- raise ValueError("Expected to start with a '%s' line:\n%s" % (expected_first_keyword, content))
-
- expected_first_keyword = None
-
- entries.setdefault(keyword, []).append(value)
-
- return entries
-
def _decode_fingerprint(identity, validate):
"""
Decodes the 'identity' value found in consensuses into the more common hex
diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py
index c21b4a2..4ca8e5c 100644
--- a/test/integ/descriptor/networkstatus.py
+++ b/test/integ/descriptor/networkstatus.py
@@ -85,7 +85,6 @@ class TestNetworkStatus(unittest.TestCase):
router1 = desc.routers[0]
descriptor_file.close()
- self.assertEquals(True, desc.validated)
self.assertEquals("3", desc.network_status_version)
self.assertEquals("consensus", desc.vote_status)
self.assertEquals([], desc.consensus_methods)
@@ -178,7 +177,6 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
router1 = desc.routers[0]
descriptor_file.close()
- self.assertEquals(True, desc.validated)
self.assertEquals("3", desc.network_status_version)
self.assertEquals("vote", desc.vote_status)
self.assertEquals(range(1, 13), desc.consensus_methods)
1
0
commit 239d9642bfc800b4f720880f359cdc92a713e63f
Author: Damian Johnson <atagar(a)torproject.org>
Date: Tue Aug 21 16:52:56 2012 -0700
RouterStatusEntry unit tests
Unit tests for the RouterStatusEntry use cases that come to mind. As normal
they uncovered some bugs with the class.
---
stem/descriptor/networkstatus.py | 44 +++-
test/unit/descriptor/networkstatus.py | 354 +++++++++++++++++++++++++++++++++
2 files changed, 386 insertions(+), 12 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 96f10ce..021ac4c 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -531,7 +531,7 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
r_comp = value.split(" ")
- if len(r_comp) < 5:
+ if len(r_comp) < 8:
if not validate: continue
raise ValueError("Router status entry's 'r' line line must have eight values: %s" % line)
@@ -564,7 +564,17 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
# "s" Flags
# s Named Running Stable Valid
- self.flags = value.split(" ")
+ if value == "":
+ self.flags = []
+ else:
+ self.flags = value.split(" ")
+
+ if validate:
+ for flag in self.flags:
+ if self.flags.count(flag) > 1:
+ raise ValueError("Router status entry had duplicate flags: %s" % line)
+ elif flag == "":
+ raise ValueError("Router status entry had extra whitespace on its 's' line: %s" % line)
elif keyword == 'v':
# "v" version
# v Tor 0.2.2.35
@@ -595,16 +605,19 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
raise ValueError("Router status entry's 'w' line needs to start with a 'Bandwidth=' entry: %s" % line)
for w_entry in w_comp:
- w_key, w_value = w_entry.split('=', 1)
+ if '=' in w_entry:
+ w_key, w_value = w_entry.split('=', 1)
+ else:
+ w_key, w_value = w_entry, None
if w_key == "Bandwidth":
- if not w_value.isdigit():
+ if not (w_value and w_value.isdigit()):
if not validate: continue
raise ValueError("Router status entry's 'Bandwidth=' entry needs to have a numeric value: %s" % line)
self.bandwidth = int(w_value)
elif w_key == "Measured":
- if not w_value.isdigit():
+ if not (w_value and w_value.isdigit()):
if not validate: continue
raise ValueError("Router status entry's 'Measured=' entry needs to have a numeric value: %s" % line)
@@ -627,9 +640,11 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
m_comp = value.split(" ")
- if self.document.vote_status != "vote":
+ if not (self.document and self.document.vote_status == "vote"):
if not validate: continue
- raise ValueError("Router status entry's 'm' line should only appear in votes (appeared in a %s): %s" % (self.document.vote_status, line))
+
+ vote_status = self.document.vote_status if self.document else "<undefined document>"
+ raise ValueError("Router status entry's 'm' line should only appear in votes (appeared in a %s): %s" % (vote_status, line))
elif len(m_comp) < 1:
if not validate: continue
raise ValueError("Router status entry's 'm' line needs to start with a series of methods: %s" % line)
@@ -884,7 +899,14 @@ def _decode_fingerprint(identity, validate):
identity += "=" * missing_padding
fingerprint = ""
- for char in base64.b64decode(identity):
+
+ try:
+ identity_decoded = base64.b64decode(identity)
+ except TypeError, exc:
+ if not validate: return None
+ raise ValueError("Unable to decode identity string '%s'" % identity)
+
+ for char in identity_decoded:
# Individual characters are either standard ascii or hex encoded, and each
# represent two hex digits. For instnace...
#
@@ -898,10 +920,8 @@ def _decode_fingerprint(identity, validate):
fingerprint += hex(ord(char))[2:].zfill(2).upper()
if not stem.util.tor_tools.is_valid_fingerprint(fingerprint):
- if validate:
- raise ValueError("Decoded '%s' to be '%s', which isn't a valid fingerprint" % (identity, fingerprint))
- else:
- return None
+ if not validate: return None
+ raise ValueError("Decoded '%s' to be '%s', which isn't a valid fingerprint" % (identity, fingerprint))
return fingerprint
diff --git a/test/unit/descriptor/networkstatus.py b/test/unit/descriptor/networkstatus.py
index e6a8514..c7672e6 100644
--- a/test/unit/descriptor/networkstatus.py
+++ b/test/unit/descriptor/networkstatus.py
@@ -6,6 +6,8 @@ import datetime
import unittest
from stem.descriptor.networkstatus import Flag, RouterStatusEntry, _decode_fingerprint
+from stem.version import Version
+from stem.exit_policy import MicrodescriptorExitPolicy
ROUTER_STATUS_ENTRY_ATTR = (
("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
@@ -90,4 +92,356 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEqual(None, entry.exit_policy)
self.assertEqual(None, entry.microdescriptor_hashes)
self.assertEqual([], entry.get_unrecognized_lines())
+
+ def test_rse_missing_fields(self):
+ """
+ Parses a router status entry that's missing fields.
+ """
+
+ content = get_router_status_entry(exclude = ('r', 's'))
+ self._expect_invalid_rse_attr(content, "address")
+
+ content = get_router_status_entry(exclude = ('r',))
+ self._expect_invalid_rse_attr(content, "address")
+
+ content = get_router_status_entry(exclude = ('s',))
+ self._expect_invalid_rse_attr(content, "flags")
+
+ def test_rse_unrecognized_lines(self):
+ """
+ Parses a router status entry with new keywords.
+ """
+
+ content = get_router_status_entry({'z': 'New tor feature: sparkly unicorns!'})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(['z New tor feature: sparkly unicorns!'], entry.get_unrecognized_lines())
+
+ def test_rse_proceeding_line(self):
+ """
+ Includes content prior to the 'r' line.
+ """
+
+ content = 'z some stuff\n' + get_router_status_entry()
+ self._expect_invalid_rse_attr(content, "_unrecognized_lines", ['z some stuff'])
+
+ def test_rse_blank_lines(self):
+ """
+ Includes blank lines, which should be ignored.
+ """
+
+ content = get_router_status_entry() + "\n\nv Tor 0.2.2.35\n\n"
+ entry = RouterStatusEntry(content, None)
+ self.assertEqual("Tor 0.2.2.35", entry.version_line)
+
+ def test_rse_missing_r_field(self):
+ """
+ Excludes fields from the 'r' line.
+ """
+
+ components = (
+ ('nickname', 'caerSidi'),
+ ('fingerprint', 'p1aag7VwarGxqctS7/fS0y5FU+s'),
+ ('digest', 'oQZFLYe9e4A7bOkWKR7TaNxb0JE'),
+ ('published', '2012-08-06 11:19:31'),
+ ('address', '71.35.150.29'),
+ ('or_port', '9001'),
+ ('dir_port', '0'),
+ )
+
+ for attr, value in components:
+ # construct the 'r' line without this field
+ test_components = [comp[1] for comp in components]
+ test_components.remove(value)
+ r_line = ' '.join(test_components)
+
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, attr)
+
+ def test_rse_malformed_nickname(self):
+ """
+ Parses an 'r' line with a malformed nickname.
+ """
+
+ test_values = (
+ "",
+ "saberrider2008ReallyLongNickname", # too long
+ "$aberrider2008", # invalid characters
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("caerSidi", value)
+ content = get_router_status_entry({'r': r_line})
+
+ # TODO: Initial whitespace is consumed as part of the keyword/value
+ # divider. This is a bug in the case of V3 router status entries, but
+ # proper behavior for V2 router status entries and server/extrainfo
+ # descriptors.
+ #
+ # I'm inclined to leave this as-is for the moment since fixing it
+ # requires special KEYWORD_LINE handling, and the only result of this bug
+ # is that our validation doesn't catch the new SP restriction on V3
+ # entries.
+
+ if value == "": value = None
+
+ self._expect_invalid_rse_attr(content, "nickname", value)
+
+ def test_rse_malformed_fingerprint(self):
+ """
+ Parses an 'r' line with a malformed fingerprint.
+ """
+
+ test_values = (
+ "",
+ "zzzzz",
+ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("p1aag7VwarGxqctS7/fS0y5FU+s", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, "fingerprint")
+
+ def test_rse_malformed_published_date(self):
+ """
+ Parses an 'r' line with a malformed published date.
+ """
+
+ test_values = (
+ "",
+ "2012-08-06 11:19:",
+ "2012-08-06 11:19:71",
+ "2012-08-06 11::31",
+ "2012-08-06 11:79:31",
+ "2012-08-06 :19:31",
+ "2012-08-06 41:19:31",
+ "2012-08- 11:19:31",
+ "2012-08-86 11:19:31",
+ "2012--06 11:19:31",
+ "2012-38-06 11:19:31",
+ "-08-06 11:19:31",
+ "2012-08-06 11:19:31",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("2012-08-06 11:19:31", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, "published")
+
+ def test_rse_malformed_address(self):
+ """
+ Parses an 'r' line with a malformed address.
+ """
+
+ test_values = (
+ "",
+ "71.35.150.",
+ "71.35..29",
+ "71.35.150",
+ "71.35.150.256",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("71.35.150.29", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, "address", value)
+
+ def test_rse_malformed_port(self):
+ """
+ Parses an 'r' line with a malformed ORPort or DirPort.
+ """
+
+ test_values = (
+ "",
+ "-1",
+ "399482",
+ "blarg",
+ )
+
+ for value in test_values:
+ for include_or_port in (False, True):
+ for include_dir_port in (False, True):
+ if not include_or_port and not include_dir_port:
+ continue
+
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1]
+
+ if include_or_port:
+ r_line = r_line.replace(" 9001 ", " %s " % value)
+
+ if include_dir_port:
+ r_line = r_line[:-1] + value
+
+ attr = "or_port" if include_or_port else "dir_port"
+ expected = int(value) if value.isdigit() else None
+
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_rse_attr(content, attr, expected)
+
+ def test_rse_flags(self):
+ """
+ Handles a variety of flag inputs.
+ """
+
+ test_values = {
+ "": [],
+ "Fast": [Flag.FAST],
+ "Fast Valid": [Flag.FAST, Flag.VALID],
+ "Ugabuga": ["Ugabuga"],
+ }
+
+ for s_line, expected in test_values.items():
+ content = get_router_status_entry({'s': s_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.flags)
+
+ # tries some invalid inputs
+ test_values = {
+ "Fast ": [Flag.FAST, "", "", ""],
+ "Fast Valid": [Flag.FAST, "", Flag.VALID],
+ "Fast Fast": [Flag.FAST, Flag.FAST],
+ }
+
+ for s_line, expected in test_values.items():
+ content = get_router_status_entry({'s': s_line})
+ self._expect_invalid_rse_attr(content, "flags", expected)
+
+ def test_rse_versions(self):
+ """
+ Handles a variety of version inputs.
+ """
+
+ test_values = {
+ "Tor 0.2.2.35": Version("0.2.2.35"),
+ "Tor 0.1.2": Version("0.1.2"),
+ "Torr new_stuff": None,
+ "new_stuff and stuff": None,
+ }
+
+ for v_line, expected in test_values.items():
+ content = get_router_status_entry({'v': v_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.version)
+ self.assertEquals(v_line, entry.version_line)
+
+ # tries an invalid input
+ content = get_router_status_entry({'v': "Tor ugabuga"})
+ self._expect_invalid_rse_attr(content, "version")
+
+ def test_rse_bandwidth(self):
+ """
+ Handles a variety of 'w' lines.
+ """
+
+ test_values = {
+ "Bandwidth=0": (0, None, []),
+ "Bandwidth=63138": (63138, None, []),
+ "Bandwidth=11111 Measured=482": (11111, 482, []),
+ "Bandwidth=11111 Measured=482 Blarg!": (11111, 482, ["Blarg!"]),
+ }
+
+ for w_line, expected in test_values.items():
+ content = get_router_status_entry({'w': w_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected[0], entry.bandwidth)
+ self.assertEquals(expected[1], entry.measured)
+ self.assertEquals(expected[2], entry.unrecognized_bandwidth_entries)
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "blarg",
+ "Bandwidth",
+ "Bandwidth=",
+ "Bandwidth:0",
+ "Bandwidth 0",
+ "Bandwidth=-10",
+ "Bandwidth=10 Measured",
+ "Bandwidth=10 Measured=",
+ "Bandwidth=10 Measured=-50",
+ )
+
+ for w_line in test_values:
+ content = get_router_status_entry({'w': w_line})
+ self._expect_invalid_rse_attr(content)
+
+ def test_rse_exit_policy(self):
+ """
+ Handles a variety of 'p' lines.
+ """
+
+ test_values = {
+ "reject 1-65535": MicrodescriptorExitPolicy("reject 1-65535"),
+ "accept 80,110,143,443": MicrodescriptorExitPolicy("accept 80,110,143,443"),
+ }
+
+ for p_line, expected in test_values.items():
+ content = get_router_status_entry({'p': p_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.exit_policy)
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "blarg",
+ "reject -50",
+ "accept 80,",
+ )
+
+ for p_line in test_values:
+ content = get_router_status_entry({'p': p_line})
+ self._expect_invalid_rse_attr(content, "exit_policy")
+
+ def test_rse_microdescriptor_hashes(self):
+ """
+ Handles a variety of 'm' lines.
+ """
+
+ test_values = {
+ "8,9,10,11,12":
+ [([8, 9, 10, 11, 12], {})],
+ "8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs":
+ [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
+ "8,9,10,11,12 sha256=g1vx9si329muxV md5=3tquWIXXySNOIwRGMeAESKs/v4DWs":
+ [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV", "md5": "3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
+ }
+
+ # we need a document that's a vote
+ mock_document = lambda x: x # just need anything with a __dict__
+ mock_document.__dict__["vote_status"] = "vote"
+
+ for m_line, expected in test_values.items():
+ content = get_router_status_entry({'m': m_line})
+ entry = RouterStatusEntry(content, mock_document)
+ self.assertEquals(expected, entry.microdescriptor_hashes)
+
+ # try without a document
+ content = get_router_status_entry({'m': "8,9,10,11,12"})
+ self._expect_invalid_rse_attr(content, "microdescriptor_hashes")
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "4,a,2",
+ "1,2,3 stuff",
+ )
+
+ for m_line in test_values:
+ content = get_router_status_entry({'m': m_line})
+ self.assertRaises(ValueError, RouterStatusEntry, content, mock_document)
+
+ def _expect_invalid_rse_attr(self, content, attr = None, expected_value = None):
+ """
+ Asserts that construction will fail due to content having a malformed
+ attribute. If an attr is provided then we check that it matches an expected
+ value when we're constructed without validation.
+ """
+
+ self.assertRaises(ValueError, RouterStatusEntry, content, None)
+ entry = RouterStatusEntry(content, None, False)
+
+ if attr:
+ self.assertEquals(expected_value, getattr(entry, attr))
+ else:
+ self.assertEquals("caerSidi", entry.nickname)
1
0

13 Oct '12
commit cf475d58dfa4d42e982eca6307e2a61e7545147e
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu Sep 6 09:14:09 2012 -0700
Minimal unit test for network status documents
Adding a unit test for the minimal valid network status document (plus a
consensus-method field since that influences validation). This uncovered
some bugs with the NetworkStatusDocument class...
* The network_status_version field misdocumented as being an int (it was
actually a str). We need it to be a str for microdescriptors so simply
changed the pydoc.
* The consensus-method and bandwidth-weights are documented in the spec as
being optional fields. The parser errored with a stacktrace when
consensus-method was missing, and gave a validation error if there isn't a
bandwidth-weights.
* Inappropriate validation error if there was unrecognized content.
* The get_unrecognized_lines() method is documented as providing a list of
lines. The NetworkStatusDocument returned a string instead.
* Off-by-one error that caused consensus-method 9 documents to skip parsing
footers.
---
stem/descriptor/networkstatus.py | 28 ++++--
test/unit/descriptor/networkstatus.py | 173 ++++++++++++++++++++++++++------
2 files changed, 159 insertions(+), 42 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 168c768..6f1f7b1 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -198,11 +198,11 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
:var tuple routers: RouterStatusEntry contained in the document
- :var int network_status_version: **\*** document version
+ :var str network_status_version: **\*** document version
:var str vote_status: **\*** status of the vote (is either "vote" or "consensus")
+ :var int consensus_method: **~** consensus method used to generate a consensus
:var list consensus_methods: **^** A list of supported consensus generation methods (integers)
:var datetime published: **^** time when the document was published
- :var int consensus_method: **~** consensus method used to generate a consensus
:var datetime valid_after: **\*** time when the consensus becomes valid
:var datetime fresh_until: **\*** time until when the consensus is considered to be fresh
:var datetime valid_until: **\*** time until when the consensus is valid
@@ -318,7 +318,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.published = _strptime(_read_keyword_line("published", content, validate, True), validate, True)
else:
read_keyword_line("consensus-method", True)
- self.consensus_method = int(self.consensus_method)
+ if self.consensus_method != None:
+ self.consensus_method = int(self.consensus_method)
map(read_keyword_line, ["valid-after", "fresh-until", "valid-until"])
self.valid_after = _strptime(self.valid_after, validate)
@@ -345,7 +346,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.directory_authorities.append(DirectoryAuthority(dirauth_data, vote, validate))
# footer section
- if self.consensus_method > 9 or vote and filter(lambda x: x >= 9, self.consensus_methods):
+ if self.consensus_method >= 9 or vote and filter(lambda x: x >= 9, self.consensus_methods):
if _peek_keyword(content) == "directory-footer":
content.readline()
elif validate:
@@ -353,17 +354,19 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
if not vote:
read_keyword_line("bandwidth-weights", True)
- if _bandwidth_weights_regex.match(self.bandwidth_weights):
+ if self.bandwidth_weights != None and _bandwidth_weights_regex.match(self.bandwidth_weights):
self.bandwidth_weights = dict([(weight.split("=")[0], int(weight.split("=")[1])) for weight in self.bandwidth_weights.split(" ")])
- elif validate:
- raise ValueError("Invalid bandwidth-weights line")
while _peek_keyword(content) == "directory-signature":
signature_data = _read_until_keywords("directory-signature", content, False, True)
self.directory_signatures.append(DirectorySignature("".join(signature_data)))
- self.unrecognized_lines = content.read()
- if validate and self.unrecognized_lines: raise ValueError("Unrecognized trailing data")
+ remainder = content.read()
+
+ if remainder:
+ self.unrecognized_lines = content.read().split("\n")
+ else:
+ self.unrecognized_lines = []
def _check_for_missing_and_disallowed_fields(self, is_consensus, header_entries, footer_entries):
"""
@@ -508,6 +511,13 @@ class DirectorySignature(stem.descriptor.Descriptor):
"""
return self.unrecognized_lines
+
+ def __cmp__(self, other):
+ if not isinstance(other, DirectorySignature):
+ return 1
+
+ # attributes are all derived from content, so we can simply use that to check
+ return str(self) > str(other)
class RouterStatusEntry(stem.descriptor.Descriptor):
"""
diff --git a/test/unit/descriptor/networkstatus.py b/test/unit/descriptor/networkstatus.py
index c7672e6..db1270e 100644
--- a/test/unit/descriptor/networkstatus.py
+++ b/test/unit/descriptor/networkstatus.py
@@ -5,15 +5,89 @@ Unit tests for stem.descriptor.networkstatus.
import datetime
import unittest
-from stem.descriptor.networkstatus import Flag, RouterStatusEntry, _decode_fingerprint
+from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS, Flag, NetworkStatusDocument, RouterStatusEntry, DirectorySignature, _decode_fingerprint
from stem.version import Version
from stem.exit_policy import MicrodescriptorExitPolicy
+NETWORK_STATUS_DOCUMENT_ATTR = {
+ "network-status-version": "3",
+ "vote-status": "consensus",
+ "consensus-method": "9",
+ "published": "2012-09-02 22:00:00",
+ "valid-after": "2012-09-02 22:00:00",
+ "fresh-until": "2012-09-02 22:00:00",
+ "valid-until": "2012-09-02 22:00:00",
+ "voting-delay": "300 300",
+ "known-flags": "Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid",
+ "directory-footer": "",
+ "directory-signature": "\n".join((
+ "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD",
+ "-----BEGIN SIGNATURE-----",
+ "e1XH33ITaUYzXu+dK04F2dZwR4PhcOQgIuK859KGpU77/6lRuggiX/INk/4FJanJ",
+ "ysCTE1K4xk4fH3N1Tzcv/x/gS4LUlIZz3yKfBnj+Xh3w12Enn9V1Gm1Vrhl+/YWH",
+ "eweONYRZTTvgsB+aYsCoBuoBBpbr4Swlu64+85F44o4=",
+ "-----END SIGNATURE-----")),
+}
+
ROUTER_STATUS_ENTRY_ATTR = (
("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
("s", "Fast Named Running Stable Valid"),
)
+def get_network_status_document(attr = None, exclude = None, routers = None):
+ """
+ Constructs a minimal network status document with the given attributes. This
+ places attributes in the proper order to be valid.
+
+ :param dict attr: keyword/value mappings to be included in the entry
+ :param list exclude: mandatory keywords to exclude from the entry
+ :param list routers: lines with router status entry content
+
+ :returns: str with customized router status entry content
+ """
+
+ descriptor_lines = []
+ if attr is None: attr = {}
+ if exclude is None: exclude = []
+ if routers is None: routers = []
+ attr = dict(attr) # shallow copy since we're destructive
+
+ is_vote = attr.get("vote-status") == "vote"
+ is_consensus = not is_vote
+
+ header_content, footer_content = [], []
+
+ for content, entries in ((header_content, HEADER_STATUS_DOCUMENT_FIELDS),
+ (footer_content, FOOTER_STATUS_DOCUMENT_FIELDS)):
+ for field, in_votes, in_consensus, is_mandatory in entries:
+ if field in exclude: continue
+
+ if not field in attr:
+ # Skip if it's not mandatory for this type of document. An exception is
+ # made for the consensus' consensus-method field since it influences
+ # validation, and is only missing for consensus-method lower than 2.
+
+ if field == "consensus-method" and is_consensus:
+ pass
+ elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_vote)):
+ continue
+
+ if field in attr:
+ value = attr[keyword]
+ del attr[keyword]
+ elif field in NETWORK_STATUS_DOCUMENT_ATTR:
+ value = NETWORK_STATUS_DOCUMENT_ATTR[field]
+
+ if value: value = " %s" % value
+ content.append(field + value)
+
+ remainder = []
+ for attr_keyword, attr_value in attr.items():
+ if attr_value: attr_value = " %s" % attr_value
+ remainder.append(attr_keyword + attr_value)
+
+ return "\n".join(header_content + remainder + routers + footer_content)
+
def get_router_status_entry(attr = None, exclude = None):
"""
Constructs a minimal router status entry with the given attributes.
@@ -67,7 +141,40 @@ class TestNetworkStatus(unittest.TestCase):
self.assertRaises(ValueError, _decode_fingerprint, arg, True)
self.assertEqual(None, _decode_fingerprint(arg, False))
- def test_rse_minimal(self):
+ def test_document_minimal(self):
+ """
+ Parses a minimal network status document.
+ """
+
+ document = NetworkStatusDocument(get_network_status_document())
+
+ expected_known_flags = [Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT,
+ Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING,
+ Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID]
+
+ sig = DirectorySignature("directory-signature " + NETWORK_STATUS_DOCUMENT_ATTR["directory-signature"])
+
+ self.assertEqual((), document.routers)
+ self.assertEqual("3", document.network_status_version)
+ self.assertEqual("consensus", document.vote_status)
+ self.assertEqual(9, document.consensus_method)
+ self.assertEqual([], document.consensus_methods)
+ self.assertEqual(None, document.published)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_after)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.fresh_until)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_until)
+ self.assertEqual(300, document.vote_delay)
+ self.assertEqual(300, document.dist_delay)
+ self.assertEqual([], document.client_versions)
+ self.assertEqual([], document.server_versions)
+ self.assertEqual(expected_known_flags, document.known_flags)
+ self.assertEqual(None, document.params)
+ self.assertEqual([], document.directory_authorities)
+ self.assertEqual(None, document.bandwidth_weights)
+ self.assertEqual([sig], document.directory_signatures)
+ self.assertEqual([], document.get_unrecognized_lines())
+
+ def test_entry_minimal(self):
"""
Parses a minimal router status entry.
"""
@@ -93,21 +200,21 @@ class TestNetworkStatus(unittest.TestCase):
self.assertEqual(None, entry.microdescriptor_hashes)
self.assertEqual([], entry.get_unrecognized_lines())
- def test_rse_missing_fields(self):
+ def test_entry_missing_fields(self):
"""
Parses a router status entry that's missing fields.
"""
content = get_router_status_entry(exclude = ('r', 's'))
- self._expect_invalid_rse_attr(content, "address")
+ self._expect_invalid_entry_attr(content, "address")
content = get_router_status_entry(exclude = ('r',))
- self._expect_invalid_rse_attr(content, "address")
+ self._expect_invalid_entry_attr(content, "address")
content = get_router_status_entry(exclude = ('s',))
- self._expect_invalid_rse_attr(content, "flags")
+ self._expect_invalid_entry_attr(content, "flags")
- def test_rse_unrecognized_lines(self):
+ def test_entry_unrecognized_lines(self):
"""
Parses a router status entry with new keywords.
"""
@@ -116,15 +223,15 @@ class TestNetworkStatus(unittest.TestCase):
entry = RouterStatusEntry(content, None)
self.assertEquals(['z New tor feature: sparkly unicorns!'], entry.get_unrecognized_lines())
- def test_rse_proceeding_line(self):
+ def test_entry_proceeding_line(self):
"""
Includes content prior to the 'r' line.
"""
content = 'z some stuff\n' + get_router_status_entry()
- self._expect_invalid_rse_attr(content, "_unrecognized_lines", ['z some stuff'])
+ self._expect_invalid_entry_attr(content, "_unrecognized_lines", ['z some stuff'])
- def test_rse_blank_lines(self):
+ def test_entry_blank_lines(self):
"""
Includes blank lines, which should be ignored.
"""
@@ -133,7 +240,7 @@ class TestNetworkStatus(unittest.TestCase):
entry = RouterStatusEntry(content, None)
self.assertEqual("Tor 0.2.2.35", entry.version_line)
- def test_rse_missing_r_field(self):
+ def test_entry_missing_r_field(self):
"""
Excludes fields from the 'r' line.
"""
@@ -155,9 +262,9 @@ class TestNetworkStatus(unittest.TestCase):
r_line = ' '.join(test_components)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, attr)
+ self._expect_invalid_entry_attr(content, attr)
- def test_rse_malformed_nickname(self):
+ def test_entry_malformed_nickname(self):
"""
Parses an 'r' line with a malformed nickname.
"""
@@ -184,9 +291,9 @@ class TestNetworkStatus(unittest.TestCase):
if value == "": value = None
- self._expect_invalid_rse_attr(content, "nickname", value)
+ self._expect_invalid_entry_attr(content, "nickname", value)
- def test_rse_malformed_fingerprint(self):
+ def test_entry_malformed_fingerprint(self):
"""
Parses an 'r' line with a malformed fingerprint.
"""
@@ -200,9 +307,9 @@ class TestNetworkStatus(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("p1aag7VwarGxqctS7/fS0y5FU+s", value)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, "fingerprint")
+ self._expect_invalid_entry_attr(content, "fingerprint")
- def test_rse_malformed_published_date(self):
+ def test_entry_malformed_published_date(self):
"""
Parses an 'r' line with a malformed published date.
"""
@@ -226,9 +333,9 @@ class TestNetworkStatus(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("2012-08-06 11:19:31", value)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, "published")
+ self._expect_invalid_entry_attr(content, "published")
- def test_rse_malformed_address(self):
+ def test_entry_malformed_address(self):
"""
Parses an 'r' line with a malformed address.
"""
@@ -244,9 +351,9 @@ class TestNetworkStatus(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("71.35.150.29", value)
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, "address", value)
+ self._expect_invalid_entry_attr(content, "address", value)
- def test_rse_malformed_port(self):
+ def test_entry_malformed_port(self):
"""
Parses an 'r' line with a malformed ORPort or DirPort.
"""
@@ -276,9 +383,9 @@ class TestNetworkStatus(unittest.TestCase):
expected = int(value) if value.isdigit() else None
content = get_router_status_entry({'r': r_line})
- self._expect_invalid_rse_attr(content, attr, expected)
+ self._expect_invalid_entry_attr(content, attr, expected)
- def test_rse_flags(self):
+ def test_entry_flags(self):
"""
Handles a variety of flag inputs.
"""
@@ -304,9 +411,9 @@ class TestNetworkStatus(unittest.TestCase):
for s_line, expected in test_values.items():
content = get_router_status_entry({'s': s_line})
- self._expect_invalid_rse_attr(content, "flags", expected)
+ self._expect_invalid_entry_attr(content, "flags", expected)
- def test_rse_versions(self):
+ def test_entry_versions(self):
"""
Handles a variety of version inputs.
"""
@@ -326,9 +433,9 @@ class TestNetworkStatus(unittest.TestCase):
# tries an invalid input
content = get_router_status_entry({'v': "Tor ugabuga"})
- self._expect_invalid_rse_attr(content, "version")
+ self._expect_invalid_entry_attr(content, "version")
- def test_rse_bandwidth(self):
+ def test_entry_bandwidth(self):
"""
Handles a variety of 'w' lines.
"""
@@ -363,9 +470,9 @@ class TestNetworkStatus(unittest.TestCase):
for w_line in test_values:
content = get_router_status_entry({'w': w_line})
- self._expect_invalid_rse_attr(content)
+ self._expect_invalid_entry_attr(content)
- def test_rse_exit_policy(self):
+ def test_entry_exit_policy(self):
"""
Handles a variety of 'p' lines.
"""
@@ -390,9 +497,9 @@ class TestNetworkStatus(unittest.TestCase):
for p_line in test_values:
content = get_router_status_entry({'p': p_line})
- self._expect_invalid_rse_attr(content, "exit_policy")
+ self._expect_invalid_entry_attr(content, "exit_policy")
- def test_rse_microdescriptor_hashes(self):
+ def test_entry_microdescriptor_hashes(self):
"""
Handles a variety of 'm' lines.
"""
@@ -417,7 +524,7 @@ class TestNetworkStatus(unittest.TestCase):
# try without a document
content = get_router_status_entry({'m': "8,9,10,11,12"})
- self._expect_invalid_rse_attr(content, "microdescriptor_hashes")
+ self._expect_invalid_entry_attr(content, "microdescriptor_hashes")
# tries some invalid inputs
test_values = (
@@ -430,7 +537,7 @@ class TestNetworkStatus(unittest.TestCase):
content = get_router_status_entry({'m': m_line})
self.assertRaises(ValueError, RouterStatusEntry, content, mock_document)
- def _expect_invalid_rse_attr(self, content, attr = None, expected_value = None):
+ def _expect_invalid_entry_attr(self, content, attr = None, expected_value = None):
"""
Asserts that construction will fail due to content having a malformed
attribute. If an attr is provided then we check that it matches an expected
1
0

13 Oct '12
commit 725e2f9deb1a22b5cfca0242ea79d5c0548a40e3
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Sep 8 10:26:48 2012 -0700
Unit tests for minimal vote and missing fields
Unit tests for a couple important use cases and lots 'o fixes for the issues
they uncovered. As mentioned earlier the 'validate' attribute took the wrong
meaning in this parser so valid content errors and invalid content triggers
stacktraces.
---
stem/descriptor/networkstatus.py | 47 ++++++++++++++---
test/unit/descriptor/networkstatus/document.py | 65 +++++++++++++++++++++--
2 files changed, 97 insertions(+), 15 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index 6f1f7b1..e55c06f 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -308,25 +308,49 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
if validate and not self._validate_network_status_version():
raise ValueError("Invalid network-status-version: %s" % self.network_status_version)
+ vote = False
if self.vote_status == "vote": vote = True
elif self.vote_status == "consensus": vote = False
elif validate: raise ValueError("Unrecognized vote-status")
if vote:
read_keyword_line("consensus-methods", True)
- self.consensus_methods = [int(method) for method in self.consensus_methods.split(" ")]
- self.published = _strptime(_read_keyword_line("published", content, validate, True), validate, True)
+
+ if self.consensus_methods:
+ self.consensus_methods = [int(method) for method in self.consensus_methods.split(" ")]
+ else:
+ self.consensus_methods = []
+
+ line = _read_keyword_line("published", content, validate, True)
+
+ if line:
+ self.published = _strptime(line, validate, True)
else:
read_keyword_line("consensus-method", True)
if self.consensus_method != None:
self.consensus_method = int(self.consensus_method)
map(read_keyword_line, ["valid-after", "fresh-until", "valid-until"])
- self.valid_after = _strptime(self.valid_after, validate)
- self.fresh_until = _strptime(self.fresh_until, validate)
- self.valid_until = _strptime(self.valid_until, validate)
+
+ if self.valid_after:
+ self.valid_after = _strptime(self.valid_after, validate)
+ elif validate:
+ raise ValueError("Missing the 'valid-after' field")
+
+ if self.fresh_until:
+ self.fresh_until = _strptime(self.fresh_until, validate)
+ elif validate:
+ raise ValueError("Missing the 'fresh-until' field")
+
+ if self.valid_until:
+ self.valid_until = _strptime(self.valid_until, validate)
+ elif validate:
+ raise ValueError("Missing the 'valid-until' field")
+
voting_delay = _read_keyword_line("voting-delay", content, validate)
- self.vote_delay, self.dist_delay = [int(delay) for delay in voting_delay.split(" ")]
+
+ if voting_delay:
+ self.vote_delay, self.dist_delay = [int(delay) for delay in voting_delay.split(" ")]
client_versions = _read_keyword_line("client-versions", content, validate, True)
if client_versions:
@@ -334,7 +358,12 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
server_versions = _read_keyword_line("server-versions", content, validate, True)
if server_versions:
self.server_versions = [stem.version.Version(version_string) for version_string in server_versions.split(",")]
- self.known_flags = _read_keyword_line("known-flags", content, validate).split(" ")
+
+ flags_content = _read_keyword_line("known-flags", content, validate)
+
+ if flags_content:
+ self.known_flags = flags_content.split(" ")
+
read_keyword_line("params", True)
if self.params:
self.params = dict([(param.split("=")[0], int(param.split("=")[1])) for param in self.params.split(" ")])
@@ -346,7 +375,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.directory_authorities.append(DirectoryAuthority(dirauth_data, vote, validate))
# footer section
- if self.consensus_method >= 9 or vote and filter(lambda x: x >= 9, self.consensus_methods):
+ if self.consensus_method >= 9 or (vote and filter(lambda x: x >= 9, self.consensus_methods)):
if _peek_keyword(content) == "directory-footer":
content.readline()
elif validate:
@@ -364,7 +393,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
remainder = content.read()
if remainder:
- self.unrecognized_lines = content.read().split("\n")
+ self.unrecognized_lines = remainder.split("\n")
else:
self.unrecognized_lines = []
diff --git a/test/unit/descriptor/networkstatus/document.py b/test/unit/descriptor/networkstatus/document.py
index 027347b..8f770bb 100644
--- a/test/unit/descriptor/networkstatus/document.py
+++ b/test/unit/descriptor/networkstatus/document.py
@@ -10,6 +10,7 @@ from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_
NETWORK_STATUS_DOCUMENT_ATTR = {
"network-status-version": "3",
"vote-status": "consensus",
+ "consensus-methods": "9",
"consensus-method": "9",
"published": "2012-09-02 22:00:00",
"valid-after": "2012-09-02 22:00:00",
@@ -57,17 +58,20 @@ def get_network_status_document(attr = None, exclude = None, routers = None):
if not field in attr:
# Skip if it's not mandatory for this type of document. An exception is
- # made for the consensus' consensus-method field since it influences
- # validation, and is only missing for consensus-method lower than 2.
+ # made for the consensus' consensus-method and consensus-methods fields
+ # since it influences validation, and is only missing for
+ # consensus-method lower than 2.
if field == "consensus-method" and is_consensus:
pass
- elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_vote)):
+ elif field == "consensus-methods" and is_vote:
+ pass
+ elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_votes)):
continue
if field in attr:
- value = attr[keyword]
- del attr[keyword]
+ value = attr[field]
+ del attr[field]
elif field in NETWORK_STATUS_DOCUMENT_ATTR:
value = NETWORK_STATUS_DOCUMENT_ATTR[field]
@@ -82,7 +86,7 @@ def get_network_status_document(attr = None, exclude = None, routers = None):
return "\n".join(header_content + remainder + routers + footer_content)
class TestNetworkStatusDocument(unittest.TestCase):
- def test_document_minimal(self):
+ def test_minimal_consensus(self):
"""
Parses a minimal network status document.
"""
@@ -114,4 +118,53 @@ class TestNetworkStatusDocument(unittest.TestCase):
self.assertEqual(None, document.bandwidth_weights)
self.assertEqual([sig], document.directory_signatures)
self.assertEqual([], document.get_unrecognized_lines())
+
+ def test_minimal_vote(self):
+ """
+ Parses a minimal network status document.
+ """
+
+ document = NetworkStatusDocument(get_network_status_document({"vote-status": "vote"}))
+
+ expected_known_flags = [Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT,
+ Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING,
+ Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID]
+
+ sig = DirectorySignature("directory-signature " + NETWORK_STATUS_DOCUMENT_ATTR["directory-signature"])
+
+ self.assertEqual((), document.routers)
+ self.assertEqual("3", document.network_status_version)
+ self.assertEqual("vote", document.vote_status)
+ self.assertEqual(None, document.consensus_method)
+ self.assertEqual([9], document.consensus_methods)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.published)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_after)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.fresh_until)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_until)
+ self.assertEqual(300, document.vote_delay)
+ self.assertEqual(300, document.dist_delay)
+ self.assertEqual([], document.client_versions)
+ self.assertEqual([], document.server_versions)
+ self.assertEqual(expected_known_flags, document.known_flags)
+ self.assertEqual(None, document.params)
+ self.assertEqual([], document.directory_authorities)
+ self.assertEqual({}, document.bandwidth_weights)
+ self.assertEqual([sig], document.directory_signatures)
+ self.assertEqual([], document.get_unrecognized_lines())
+
+ def test_missing_fields(self):
+ """
+ Excludes mandatory fields from both a vote and consensus document.
+ """
+
+ for is_consensus in (True, False):
+ attr = {"vote-status": "consensus"} if is_consensus else {"vote-status": "vote"}
+ is_vote = not is_consensus
+
+ for entries in (HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS):
+ for field, in_votes, in_consensus, is_mandatory in entries:
+ if is_mandatory and ((is_consensus and in_consensus) or (is_vote and in_votes)):
+ content = get_network_status_document(attr, exclude = (field,))
+ self.assertRaises(ValueError, NetworkStatusDocument, content)
+ NetworkStatusDocument(content, False) # constructs without validation
1
0

13 Oct '12
commit f23c5312b671dd2bd41baa5a0c24568592ac3241
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Sep 8 11:23:08 2012 -0700
Validating the order of fields in documents
As mentioned in the prior commit, to be valid network status fields need to
appear in a particular order. Checking for this as part of document validation.
---
stem/descriptor/networkstatus.py | 33 ++++++++++++++++++++++++++++++++-
1 files changed, 32 insertions(+), 1 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index e55c06f..5676c16 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -255,8 +255,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
document_file = StringIO(raw_content)
header, footer, routers_end = _get_document_content(document_file, validate)
- self._parse(header, footer, validate)
self._parse_old(header + footer, validate)
+ self._parse(header, footer, validate)
if document_file.tell() < routers_end:
self.routers = tuple(_get_routers(document_file, validate, self, routers_end, self._get_router_type()))
@@ -298,6 +298,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
is_consensus = header_entries['vote-status'][0][0] == "consensus"
self._check_for_missing_and_disallowed_fields(is_consensus, header_entries, footer_entries)
+ self._check_for_misordered_fields(is_consensus, header_entries, footer_entries)
def _parse_old(self, raw_content, validate):
# preamble
@@ -430,6 +431,36 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
if disallowed_fields:
raise ValueError("Network status document has fields that shouldn't appear in this document type: %s" % ', '.join(disallowed_fields))
+
+ def _check_for_misordered_fields(self, is_consensus, header_entries, footer_entries):
+ """
+ To be valid a network status document's fiends need to appear in a specific
+ order. Checks that known fields appear in that order (unrecognized fields
+ are ignored).
+ """
+
+ # Earlier validation has ensured that our fields either belong to our
+ # document type or are unknown. Remove the unknown fields since they
+ # reflect a spec change and can appear anywhere in the document.
+
+ expected_header = [attr[0] for attr in HEADER_STATUS_DOCUMENT_FIELDS]
+ expected_footer = [attr[0] for attr in FOOTER_STATUS_DOCUMENT_FIELDS]
+
+ actual_header = filter(lambda field: field in expected_header, header_entries.keys())
+ actual_footer = filter(lambda field: field in expected_footer, footer_entries.keys())
+
+ # Narrow the expected_header and expected_footer to just what we have. If
+ # the lists then match then the order's valid.
+
+ expected_header = filter(lambda field: field in actual_header, expected_header)
+ expected_footer = filter(lambda field: field in actual_footer, expected_footer)
+
+ for label, actual, expected in (('header', actual_header, expected_header),
+ ('footer', actual_footer, expected_footer)):
+ if actual != expected:
+ actual_label = ', '.join(actual)
+ expected_label = ', '.join(expected)
+ raise ValueError("The fields in the document's %s are misordered. It should be '%s' but was '%s'" % (lable, actual_label, expected_label))
class DirectoryAuthority(stem.descriptor.Descriptor):
"""
1
0
commit bc09f1e697f4a34475b4b9b55bcb91c14494427b
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Sep 8 10:51:51 2012 -0700
Unit test for misordered fields
To be valid a network status document's fiends need to appear in a particular
order. I'm about to add vaidation for this, but the parser is being finicky.
I'm tired of trying to fix incrementally fix it, so skipping this test for now
and moving on to the parser rewrite.
---
test/unit/descriptor/networkstatus/document.py | 19 +++++++++++++++++++
1 files changed, 19 insertions(+), 0 deletions(-)
diff --git a/test/unit/descriptor/networkstatus/document.py b/test/unit/descriptor/networkstatus/document.py
index 8f770bb..d2bbe90 100644
--- a/test/unit/descriptor/networkstatus/document.py
+++ b/test/unit/descriptor/networkstatus/document.py
@@ -167,4 +167,23 @@ class TestNetworkStatusDocument(unittest.TestCase):
content = get_network_status_document(attr, exclude = (field,))
self.assertRaises(ValueError, NetworkStatusDocument, content)
NetworkStatusDocument(content, False) # constructs without validation
+
+ def test_misordered_fields(self):
+ """
+ Rearranges our descriptor fields.
+ """
+
+ self.skipTest("Needs a parser rewrite first")
+ for is_consensus in (True, False):
+ attr = {"vote-status": "consensus"} if is_consensus else {"vote-status": "vote"}
+ lines = get_network_status_document(attr).split("\n")
+
+ for i in xrange(len(lines) - 1):
+ # swaps this line with the one after it
+ test_lines = list(lines)
+ test_lines[i], test_lines[i + 1] = test_lines[i + 1], test_lines[i]
+
+ content = "\n".join(test_lines)
+ self.assertRaises(ValueError, NetworkStatusDocument, content)
+ NetworkStatusDocument(content, False) # constructs without validation
1
0

13 Oct '12
commit 7b38323ee41aa890f5b777eff4bd365d5a6c1fb1
Author: Damian Johnson <atagar(a)torproject.org>
Date: Thu Sep 6 09:29:25 2012 -0700
Splitting up entry and document unit tests
Splitting up unit tests for the RouterStatusEntry and NetworkStatusDocuemnt.
Both will be pretty lengthy and there's little reason to try to mash them
together.
---
run_tests.py | 6 +-
test/unit/descriptor/networkstatus.py | 554 ------------------------
test/unit/descriptor/networkstatus/__init__.py | 6 +
test/unit/descriptor/networkstatus/document.py | 117 +++++
test/unit/descriptor/networkstatus/entry.py | 447 +++++++++++++++++++
5 files changed, 574 insertions(+), 556 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index d2e4da7..af2d09f 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -20,7 +20,8 @@ import test.unit.descriptor.export
import test.unit.descriptor.reader
import test.unit.descriptor.server_descriptor
import test.unit.descriptor.extrainfo_descriptor
-import test.unit.descriptor.networkstatus
+import test.unit.descriptor.networkstatus.entry
+import test.unit.descriptor.networkstatus.document
import test.unit.response.control_line
import test.unit.response.control_message
import test.unit.response.getinfo
@@ -115,7 +116,8 @@ UNIT_TESTS = (
test.unit.descriptor.reader.TestDescriptorReader,
test.unit.descriptor.server_descriptor.TestServerDescriptor,
test.unit.descriptor.extrainfo_descriptor.TestExtraInfoDescriptor,
- test.unit.descriptor.networkstatus.TestNetworkStatus,
+ test.unit.descriptor.networkstatus.entry.TestRouterStatusEntry,
+ test.unit.descriptor.networkstatus.document.TestNetworkStatusDocument,
test.unit.exit_policy.rule.TestExitPolicyRule,
test.unit.exit_policy.policy.TestExitPolicy,
test.unit.version.TestVersion,
diff --git a/test/unit/descriptor/networkstatus.py b/test/unit/descriptor/networkstatus.py
deleted file mode 100644
index db1270e..0000000
--- a/test/unit/descriptor/networkstatus.py
+++ /dev/null
@@ -1,554 +0,0 @@
-"""
-Unit tests for stem.descriptor.networkstatus.
-"""
-
-import datetime
-import unittest
-
-from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS, Flag, NetworkStatusDocument, RouterStatusEntry, DirectorySignature, _decode_fingerprint
-from stem.version import Version
-from stem.exit_policy import MicrodescriptorExitPolicy
-
-NETWORK_STATUS_DOCUMENT_ATTR = {
- "network-status-version": "3",
- "vote-status": "consensus",
- "consensus-method": "9",
- "published": "2012-09-02 22:00:00",
- "valid-after": "2012-09-02 22:00:00",
- "fresh-until": "2012-09-02 22:00:00",
- "valid-until": "2012-09-02 22:00:00",
- "voting-delay": "300 300",
- "known-flags": "Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid",
- "directory-footer": "",
- "directory-signature": "\n".join((
- "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD",
- "-----BEGIN SIGNATURE-----",
- "e1XH33ITaUYzXu+dK04F2dZwR4PhcOQgIuK859KGpU77/6lRuggiX/INk/4FJanJ",
- "ysCTE1K4xk4fH3N1Tzcv/x/gS4LUlIZz3yKfBnj+Xh3w12Enn9V1Gm1Vrhl+/YWH",
- "eweONYRZTTvgsB+aYsCoBuoBBpbr4Swlu64+85F44o4=",
- "-----END SIGNATURE-----")),
-}
-
-ROUTER_STATUS_ENTRY_ATTR = (
- ("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
- ("s", "Fast Named Running Stable Valid"),
-)
-
-def get_network_status_document(attr = None, exclude = None, routers = None):
- """
- Constructs a minimal network status document with the given attributes. This
- places attributes in the proper order to be valid.
-
- :param dict attr: keyword/value mappings to be included in the entry
- :param list exclude: mandatory keywords to exclude from the entry
- :param list routers: lines with router status entry content
-
- :returns: str with customized router status entry content
- """
-
- descriptor_lines = []
- if attr is None: attr = {}
- if exclude is None: exclude = []
- if routers is None: routers = []
- attr = dict(attr) # shallow copy since we're destructive
-
- is_vote = attr.get("vote-status") == "vote"
- is_consensus = not is_vote
-
- header_content, footer_content = [], []
-
- for content, entries in ((header_content, HEADER_STATUS_DOCUMENT_FIELDS),
- (footer_content, FOOTER_STATUS_DOCUMENT_FIELDS)):
- for field, in_votes, in_consensus, is_mandatory in entries:
- if field in exclude: continue
-
- if not field in attr:
- # Skip if it's not mandatory for this type of document. An exception is
- # made for the consensus' consensus-method field since it influences
- # validation, and is only missing for consensus-method lower than 2.
-
- if field == "consensus-method" and is_consensus:
- pass
- elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_vote)):
- continue
-
- if field in attr:
- value = attr[keyword]
- del attr[keyword]
- elif field in NETWORK_STATUS_DOCUMENT_ATTR:
- value = NETWORK_STATUS_DOCUMENT_ATTR[field]
-
- if value: value = " %s" % value
- content.append(field + value)
-
- remainder = []
- for attr_keyword, attr_value in attr.items():
- if attr_value: attr_value = " %s" % attr_value
- remainder.append(attr_keyword + attr_value)
-
- return "\n".join(header_content + remainder + routers + footer_content)
-
-def get_router_status_entry(attr = None, exclude = None):
- """
- Constructs a minimal router status entry with the given attributes.
-
- :param dict attr: keyword/value mappings to be included in the entry
- :param list exclude: mandatory keywords to exclude from the entry
-
- :returns: str with customized router status entry content
- """
-
- descriptor_lines = []
- if attr is None: attr = {}
- if exclude is None: exclude = []
- attr = dict(attr) # shallow copy since we're destructive
-
- for keyword, value in ROUTER_STATUS_ENTRY_ATTR:
- if keyword in exclude: continue
- elif keyword in attr:
- value = attr[keyword]
- del attr[keyword]
-
- descriptor_lines.append("%s %s" % (keyword, value))
-
- # dump in any unused attributes
- for attr_keyword, attr_value in attr.items():
- descriptor_lines.append("%s %s" % (attr_keyword, attr_value))
-
- return "\n".join(descriptor_lines)
-
-class TestNetworkStatus(unittest.TestCase):
- def test_fingerprint_decoding(self):
- """
- Tests for the _decode_fingerprint() helper.
- """
-
- # consensus identity field and fingerprint for caerSidi and Amunet1-5
- test_values = {
- 'p1aag7VwarGxqctS7/fS0y5FU+s': 'A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB',
- 'IbhGa8T+8tyy/MhxCk/qI+EI2LU': '21B8466BC4FEF2DCB2FCC8710A4FEA23E108D8B5',
- '20wYcbFGwFfMktmuffYj6Z1RM9k': 'DB4C1871B146C057CC92D9AE7DF623E99D5133D9',
- 'nTv9AG1cZeFW2hXiSIEAF6JLRJ4': '9D3BFD006D5C65E156DA15E248810017A24B449E',
- '/UKsQiOSGPi/6es0/ha1prNTeDI': 'FD42AC42239218F8BFE9EB34FE16B5A6B3537832',
- '/nHdqoKZ6bKZixxAPzYt9Qen+Is': 'FE71DDAA8299E9B2998B1C403F362DF507A7F88B',
- }
-
- for arg, expected in test_values.items():
- self.assertEqual(expected, _decode_fingerprint(arg, True))
-
- # checks with some malformed inputs
- for arg in ('', '20wYcb', '20wYcb' * 30):
- self.assertRaises(ValueError, _decode_fingerprint, arg, True)
- self.assertEqual(None, _decode_fingerprint(arg, False))
-
- def test_document_minimal(self):
- """
- Parses a minimal network status document.
- """
-
- document = NetworkStatusDocument(get_network_status_document())
-
- expected_known_flags = [Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT,
- Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING,
- Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID]
-
- sig = DirectorySignature("directory-signature " + NETWORK_STATUS_DOCUMENT_ATTR["directory-signature"])
-
- self.assertEqual((), document.routers)
- self.assertEqual("3", document.network_status_version)
- self.assertEqual("consensus", document.vote_status)
- self.assertEqual(9, document.consensus_method)
- self.assertEqual([], document.consensus_methods)
- self.assertEqual(None, document.published)
- self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_after)
- self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.fresh_until)
- self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_until)
- self.assertEqual(300, document.vote_delay)
- self.assertEqual(300, document.dist_delay)
- self.assertEqual([], document.client_versions)
- self.assertEqual([], document.server_versions)
- self.assertEqual(expected_known_flags, document.known_flags)
- self.assertEqual(None, document.params)
- self.assertEqual([], document.directory_authorities)
- self.assertEqual(None, document.bandwidth_weights)
- self.assertEqual([sig], document.directory_signatures)
- self.assertEqual([], document.get_unrecognized_lines())
-
- def test_entry_minimal(self):
- """
- Parses a minimal router status entry.
- """
-
- entry = RouterStatusEntry(get_router_status_entry(), None)
-
- expected_flags = set([Flag.FAST, Flag.NAMED, Flag.RUNNING, Flag.STABLE, Flag.VALID])
- self.assertEqual(None, entry.document)
- self.assertEqual("caerSidi", entry.nickname)
- self.assertEqual("A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB", entry.fingerprint)
- self.assertEqual("oQZFLYe9e4A7bOkWKR7TaNxb0JE", entry.digest)
- self.assertEqual(datetime.datetime(2012, 8, 6, 11, 19, 31), entry.published)
- self.assertEqual("71.35.150.29", entry.address)
- self.assertEqual(9001, entry.or_port)
- self.assertEqual(None, entry.dir_port)
- self.assertEqual(expected_flags, set(entry.flags))
- self.assertEqual(None, entry.version_line)
- self.assertEqual(None, entry.version)
- self.assertEqual(None, entry.bandwidth)
- 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.get_unrecognized_lines())
-
- def test_entry_missing_fields(self):
- """
- Parses a router status entry that's missing fields.
- """
-
- content = get_router_status_entry(exclude = ('r', 's'))
- self._expect_invalid_entry_attr(content, "address")
-
- content = get_router_status_entry(exclude = ('r',))
- self._expect_invalid_entry_attr(content, "address")
-
- content = get_router_status_entry(exclude = ('s',))
- self._expect_invalid_entry_attr(content, "flags")
-
- def test_entry_unrecognized_lines(self):
- """
- Parses a router status entry with new keywords.
- """
-
- content = get_router_status_entry({'z': 'New tor feature: sparkly unicorns!'})
- entry = RouterStatusEntry(content, None)
- self.assertEquals(['z New tor feature: sparkly unicorns!'], entry.get_unrecognized_lines())
-
- def test_entry_proceeding_line(self):
- """
- Includes content prior to the 'r' line.
- """
-
- content = 'z some stuff\n' + get_router_status_entry()
- self._expect_invalid_entry_attr(content, "_unrecognized_lines", ['z some stuff'])
-
- def test_entry_blank_lines(self):
- """
- Includes blank lines, which should be ignored.
- """
-
- content = get_router_status_entry() + "\n\nv Tor 0.2.2.35\n\n"
- entry = RouterStatusEntry(content, None)
- self.assertEqual("Tor 0.2.2.35", entry.version_line)
-
- def test_entry_missing_r_field(self):
- """
- Excludes fields from the 'r' line.
- """
-
- components = (
- ('nickname', 'caerSidi'),
- ('fingerprint', 'p1aag7VwarGxqctS7/fS0y5FU+s'),
- ('digest', 'oQZFLYe9e4A7bOkWKR7TaNxb0JE'),
- ('published', '2012-08-06 11:19:31'),
- ('address', '71.35.150.29'),
- ('or_port', '9001'),
- ('dir_port', '0'),
- )
-
- for attr, value in components:
- # construct the 'r' line without this field
- test_components = [comp[1] for comp in components]
- test_components.remove(value)
- r_line = ' '.join(test_components)
-
- content = get_router_status_entry({'r': r_line})
- self._expect_invalid_entry_attr(content, attr)
-
- def test_entry_malformed_nickname(self):
- """
- Parses an 'r' line with a malformed nickname.
- """
-
- test_values = (
- "",
- "saberrider2008ReallyLongNickname", # too long
- "$aberrider2008", # invalid characters
- )
-
- for value in test_values:
- r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("caerSidi", value)
- content = get_router_status_entry({'r': r_line})
-
- # TODO: Initial whitespace is consumed as part of the keyword/value
- # divider. This is a bug in the case of V3 router status entries, but
- # proper behavior for V2 router status entries and server/extrainfo
- # descriptors.
- #
- # I'm inclined to leave this as-is for the moment since fixing it
- # requires special KEYWORD_LINE handling, and the only result of this bug
- # is that our validation doesn't catch the new SP restriction on V3
- # entries.
-
- if value == "": value = None
-
- self._expect_invalid_entry_attr(content, "nickname", value)
-
- def test_entry_malformed_fingerprint(self):
- """
- Parses an 'r' line with a malformed fingerprint.
- """
-
- test_values = (
- "",
- "zzzzz",
- "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
- )
-
- for value in test_values:
- r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("p1aag7VwarGxqctS7/fS0y5FU+s", value)
- content = get_router_status_entry({'r': r_line})
- self._expect_invalid_entry_attr(content, "fingerprint")
-
- def test_entry_malformed_published_date(self):
- """
- Parses an 'r' line with a malformed published date.
- """
-
- test_values = (
- "",
- "2012-08-06 11:19:",
- "2012-08-06 11:19:71",
- "2012-08-06 11::31",
- "2012-08-06 11:79:31",
- "2012-08-06 :19:31",
- "2012-08-06 41:19:31",
- "2012-08- 11:19:31",
- "2012-08-86 11:19:31",
- "2012--06 11:19:31",
- "2012-38-06 11:19:31",
- "-08-06 11:19:31",
- "2012-08-06 11:19:31",
- )
-
- for value in test_values:
- r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("2012-08-06 11:19:31", value)
- content = get_router_status_entry({'r': r_line})
- self._expect_invalid_entry_attr(content, "published")
-
- def test_entry_malformed_address(self):
- """
- Parses an 'r' line with a malformed address.
- """
-
- test_values = (
- "",
- "71.35.150.",
- "71.35..29",
- "71.35.150",
- "71.35.150.256",
- )
-
- for value in test_values:
- r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("71.35.150.29", value)
- content = get_router_status_entry({'r': r_line})
- self._expect_invalid_entry_attr(content, "address", value)
-
- def test_entry_malformed_port(self):
- """
- Parses an 'r' line with a malformed ORPort or DirPort.
- """
-
- test_values = (
- "",
- "-1",
- "399482",
- "blarg",
- )
-
- for value in test_values:
- for include_or_port in (False, True):
- for include_dir_port in (False, True):
- if not include_or_port and not include_dir_port:
- continue
-
- r_line = ROUTER_STATUS_ENTRY_ATTR[0][1]
-
- if include_or_port:
- r_line = r_line.replace(" 9001 ", " %s " % value)
-
- if include_dir_port:
- r_line = r_line[:-1] + value
-
- attr = "or_port" if include_or_port else "dir_port"
- expected = int(value) if value.isdigit() else None
-
- content = get_router_status_entry({'r': r_line})
- self._expect_invalid_entry_attr(content, attr, expected)
-
- def test_entry_flags(self):
- """
- Handles a variety of flag inputs.
- """
-
- test_values = {
- "": [],
- "Fast": [Flag.FAST],
- "Fast Valid": [Flag.FAST, Flag.VALID],
- "Ugabuga": ["Ugabuga"],
- }
-
- for s_line, expected in test_values.items():
- content = get_router_status_entry({'s': s_line})
- entry = RouterStatusEntry(content, None)
- self.assertEquals(expected, entry.flags)
-
- # tries some invalid inputs
- test_values = {
- "Fast ": [Flag.FAST, "", "", ""],
- "Fast Valid": [Flag.FAST, "", Flag.VALID],
- "Fast Fast": [Flag.FAST, Flag.FAST],
- }
-
- for s_line, expected in test_values.items():
- content = get_router_status_entry({'s': s_line})
- self._expect_invalid_entry_attr(content, "flags", expected)
-
- def test_entry_versions(self):
- """
- Handles a variety of version inputs.
- """
-
- test_values = {
- "Tor 0.2.2.35": Version("0.2.2.35"),
- "Tor 0.1.2": Version("0.1.2"),
- "Torr new_stuff": None,
- "new_stuff and stuff": None,
- }
-
- for v_line, expected in test_values.items():
- content = get_router_status_entry({'v': v_line})
- entry = RouterStatusEntry(content, None)
- self.assertEquals(expected, entry.version)
- self.assertEquals(v_line, entry.version_line)
-
- # tries an invalid input
- content = get_router_status_entry({'v': "Tor ugabuga"})
- self._expect_invalid_entry_attr(content, "version")
-
- def test_entry_bandwidth(self):
- """
- Handles a variety of 'w' lines.
- """
-
- test_values = {
- "Bandwidth=0": (0, None, []),
- "Bandwidth=63138": (63138, None, []),
- "Bandwidth=11111 Measured=482": (11111, 482, []),
- "Bandwidth=11111 Measured=482 Blarg!": (11111, 482, ["Blarg!"]),
- }
-
- for w_line, expected in test_values.items():
- content = get_router_status_entry({'w': w_line})
- entry = RouterStatusEntry(content, None)
- self.assertEquals(expected[0], entry.bandwidth)
- self.assertEquals(expected[1], entry.measured)
- self.assertEquals(expected[2], entry.unrecognized_bandwidth_entries)
-
- # tries some invalid inputs
- test_values = (
- "",
- "blarg",
- "Bandwidth",
- "Bandwidth=",
- "Bandwidth:0",
- "Bandwidth 0",
- "Bandwidth=-10",
- "Bandwidth=10 Measured",
- "Bandwidth=10 Measured=",
- "Bandwidth=10 Measured=-50",
- )
-
- for w_line in test_values:
- content = get_router_status_entry({'w': w_line})
- self._expect_invalid_entry_attr(content)
-
- def test_entry_exit_policy(self):
- """
- Handles a variety of 'p' lines.
- """
-
- test_values = {
- "reject 1-65535": MicrodescriptorExitPolicy("reject 1-65535"),
- "accept 80,110,143,443": MicrodescriptorExitPolicy("accept 80,110,143,443"),
- }
-
- for p_line, expected in test_values.items():
- content = get_router_status_entry({'p': p_line})
- entry = RouterStatusEntry(content, None)
- self.assertEquals(expected, entry.exit_policy)
-
- # tries some invalid inputs
- test_values = (
- "",
- "blarg",
- "reject -50",
- "accept 80,",
- )
-
- for p_line in test_values:
- content = get_router_status_entry({'p': p_line})
- self._expect_invalid_entry_attr(content, "exit_policy")
-
- def test_entry_microdescriptor_hashes(self):
- """
- Handles a variety of 'm' lines.
- """
-
- test_values = {
- "8,9,10,11,12":
- [([8, 9, 10, 11, 12], {})],
- "8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs":
- [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
- "8,9,10,11,12 sha256=g1vx9si329muxV md5=3tquWIXXySNOIwRGMeAESKs/v4DWs":
- [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV", "md5": "3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
- }
-
- # we need a document that's a vote
- mock_document = lambda x: x # just need anything with a __dict__
- mock_document.__dict__["vote_status"] = "vote"
-
- for m_line, expected in test_values.items():
- content = get_router_status_entry({'m': m_line})
- entry = RouterStatusEntry(content, mock_document)
- self.assertEquals(expected, entry.microdescriptor_hashes)
-
- # try without a document
- content = get_router_status_entry({'m': "8,9,10,11,12"})
- self._expect_invalid_entry_attr(content, "microdescriptor_hashes")
-
- # tries some invalid inputs
- test_values = (
- "",
- "4,a,2",
- "1,2,3 stuff",
- )
-
- for m_line in test_values:
- content = get_router_status_entry({'m': m_line})
- self.assertRaises(ValueError, RouterStatusEntry, content, mock_document)
-
- def _expect_invalid_entry_attr(self, content, attr = None, expected_value = None):
- """
- Asserts that construction will fail due to content having a malformed
- attribute. If an attr is provided then we check that it matches an expected
- value when we're constructed without validation.
- """
-
- self.assertRaises(ValueError, RouterStatusEntry, content, None)
- entry = RouterStatusEntry(content, None, False)
-
- if attr:
- self.assertEquals(expected_value, getattr(entry, attr))
- else:
- self.assertEquals("caerSidi", entry.nickname)
-
diff --git a/test/unit/descriptor/networkstatus/__init__.py b/test/unit/descriptor/networkstatus/__init__.py
new file mode 100644
index 0000000..d8483e2
--- /dev/null
+++ b/test/unit/descriptor/networkstatus/__init__.py
@@ -0,0 +1,6 @@
+"""
+Unit tests for stem.descriptor.networkstatus.
+"""
+
+__all__ = ["entry", "document"]
+
diff --git a/test/unit/descriptor/networkstatus/document.py b/test/unit/descriptor/networkstatus/document.py
new file mode 100644
index 0000000..027347b
--- /dev/null
+++ b/test/unit/descriptor/networkstatus/document.py
@@ -0,0 +1,117 @@
+"""
+Unit tests for the NetworkStatusDocument of stem.descriptor.networkstatus.
+"""
+
+import datetime
+import unittest
+
+from stem.descriptor.networkstatus import HEADER_STATUS_DOCUMENT_FIELDS, FOOTER_STATUS_DOCUMENT_FIELDS, Flag, NetworkStatusDocument, DirectorySignature
+
+NETWORK_STATUS_DOCUMENT_ATTR = {
+ "network-status-version": "3",
+ "vote-status": "consensus",
+ "consensus-method": "9",
+ "published": "2012-09-02 22:00:00",
+ "valid-after": "2012-09-02 22:00:00",
+ "fresh-until": "2012-09-02 22:00:00",
+ "valid-until": "2012-09-02 22:00:00",
+ "voting-delay": "300 300",
+ "known-flags": "Authority BadExit Exit Fast Guard HSDir Named Running Stable Unnamed V2Dir Valid",
+ "directory-footer": "",
+ "directory-signature": "\n".join((
+ "14C131DFC5C6F93646BE72FA1401C02A8DF2E8B4 BF112F1C6D5543CFD0A32215ACABD4197B5279AD",
+ "-----BEGIN SIGNATURE-----",
+ "e1XH33ITaUYzXu+dK04F2dZwR4PhcOQgIuK859KGpU77/6lRuggiX/INk/4FJanJ",
+ "ysCTE1K4xk4fH3N1Tzcv/x/gS4LUlIZz3yKfBnj+Xh3w12Enn9V1Gm1Vrhl+/YWH",
+ "eweONYRZTTvgsB+aYsCoBuoBBpbr4Swlu64+85F44o4=",
+ "-----END SIGNATURE-----")),
+}
+
+def get_network_status_document(attr = None, exclude = None, routers = None):
+ """
+ Constructs a minimal network status document with the given attributes. This
+ places attributes in the proper order to be valid.
+
+ :param dict attr: keyword/value mappings to be included in the entry
+ :param list exclude: mandatory keywords to exclude from the entry
+ :param list routers: lines with router status entry content
+
+ :returns: str with customized router status entry content
+ """
+
+ descriptor_lines = []
+ if attr is None: attr = {}
+ if exclude is None: exclude = []
+ if routers is None: routers = []
+ attr = dict(attr) # shallow copy since we're destructive
+
+ is_vote = attr.get("vote-status") == "vote"
+ is_consensus = not is_vote
+
+ header_content, footer_content = [], []
+
+ for content, entries in ((header_content, HEADER_STATUS_DOCUMENT_FIELDS),
+ (footer_content, FOOTER_STATUS_DOCUMENT_FIELDS)):
+ for field, in_votes, in_consensus, is_mandatory in entries:
+ if field in exclude: continue
+
+ if not field in attr:
+ # Skip if it's not mandatory for this type of document. An exception is
+ # made for the consensus' consensus-method field since it influences
+ # validation, and is only missing for consensus-method lower than 2.
+
+ if field == "consensus-method" and is_consensus:
+ pass
+ elif not is_mandatory or not ((is_consensus and in_consensus) or (is_vote and in_vote)):
+ continue
+
+ if field in attr:
+ value = attr[keyword]
+ del attr[keyword]
+ elif field in NETWORK_STATUS_DOCUMENT_ATTR:
+ value = NETWORK_STATUS_DOCUMENT_ATTR[field]
+
+ if value: value = " %s" % value
+ content.append(field + value)
+
+ remainder = []
+ for attr_keyword, attr_value in attr.items():
+ if attr_value: attr_value = " %s" % attr_value
+ remainder.append(attr_keyword + attr_value)
+
+ return "\n".join(header_content + remainder + routers + footer_content)
+
+class TestNetworkStatusDocument(unittest.TestCase):
+ def test_document_minimal(self):
+ """
+ Parses a minimal network status document.
+ """
+
+ document = NetworkStatusDocument(get_network_status_document())
+
+ expected_known_flags = [Flag.AUTHORITY, Flag.BADEXIT, Flag.EXIT,
+ Flag.FAST, Flag.GUARD, Flag.HSDIR, Flag.NAMED, Flag.RUNNING,
+ Flag.STABLE, Flag.UNNAMED, Flag.V2DIR, Flag.VALID]
+
+ sig = DirectorySignature("directory-signature " + NETWORK_STATUS_DOCUMENT_ATTR["directory-signature"])
+
+ self.assertEqual((), document.routers)
+ self.assertEqual("3", document.network_status_version)
+ self.assertEqual("consensus", document.vote_status)
+ self.assertEqual(9, document.consensus_method)
+ self.assertEqual([], document.consensus_methods)
+ self.assertEqual(None, document.published)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_after)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.fresh_until)
+ self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.valid_until)
+ self.assertEqual(300, document.vote_delay)
+ self.assertEqual(300, document.dist_delay)
+ self.assertEqual([], document.client_versions)
+ self.assertEqual([], document.server_versions)
+ self.assertEqual(expected_known_flags, document.known_flags)
+ self.assertEqual(None, document.params)
+ self.assertEqual([], document.directory_authorities)
+ self.assertEqual(None, document.bandwidth_weights)
+ self.assertEqual([sig], document.directory_signatures)
+ self.assertEqual([], document.get_unrecognized_lines())
+
diff --git a/test/unit/descriptor/networkstatus/entry.py b/test/unit/descriptor/networkstatus/entry.py
new file mode 100644
index 0000000..7eb0cb8
--- /dev/null
+++ b/test/unit/descriptor/networkstatus/entry.py
@@ -0,0 +1,447 @@
+"""
+Unit tests for the RouterStatusEntry of stem.descriptor.networkstatus.
+"""
+
+import datetime
+import unittest
+
+from stem.descriptor.networkstatus import Flag, RouterStatusEntry, _decode_fingerprint
+from stem.version import Version
+from stem.exit_policy import MicrodescriptorExitPolicy
+
+ROUTER_STATUS_ENTRY_ATTR = (
+ ("r", "caerSidi p1aag7VwarGxqctS7/fS0y5FU+s oQZFLYe9e4A7bOkWKR7TaNxb0JE 2012-08-06 11:19:31 71.35.150.29 9001 0"),
+ ("s", "Fast Named Running Stable Valid"),
+)
+
+def get_router_status_entry(attr = None, exclude = None):
+ """
+ Constructs a minimal router status entry with the given attributes.
+
+ :param dict attr: keyword/value mappings to be included in the entry
+ :param list exclude: mandatory keywords to exclude from the entry
+
+ :returns: str with customized router status entry content
+ """
+
+ descriptor_lines = []
+ if attr is None: attr = {}
+ if exclude is None: exclude = []
+ attr = dict(attr) # shallow copy since we're destructive
+
+ for keyword, value in ROUTER_STATUS_ENTRY_ATTR:
+ if keyword in exclude: continue
+ elif keyword in attr:
+ value = attr[keyword]
+ del attr[keyword]
+
+ descriptor_lines.append("%s %s" % (keyword, value))
+
+ # dump in any unused attributes
+ for attr_keyword, attr_value in attr.items():
+ descriptor_lines.append("%s %s" % (attr_keyword, attr_value))
+
+ return "\n".join(descriptor_lines)
+
+class TestRouterStatusEntry(unittest.TestCase):
+ def test_fingerprint_decoding(self):
+ """
+ Tests for the _decode_fingerprint() helper.
+ """
+
+ # consensus identity field and fingerprint for caerSidi and Amunet1-5
+ test_values = {
+ 'p1aag7VwarGxqctS7/fS0y5FU+s': 'A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB',
+ 'IbhGa8T+8tyy/MhxCk/qI+EI2LU': '21B8466BC4FEF2DCB2FCC8710A4FEA23E108D8B5',
+ '20wYcbFGwFfMktmuffYj6Z1RM9k': 'DB4C1871B146C057CC92D9AE7DF623E99D5133D9',
+ 'nTv9AG1cZeFW2hXiSIEAF6JLRJ4': '9D3BFD006D5C65E156DA15E248810017A24B449E',
+ '/UKsQiOSGPi/6es0/ha1prNTeDI': 'FD42AC42239218F8BFE9EB34FE16B5A6B3537832',
+ '/nHdqoKZ6bKZixxAPzYt9Qen+Is': 'FE71DDAA8299E9B2998B1C403F362DF507A7F88B',
+ }
+
+ for arg, expected in test_values.items():
+ self.assertEqual(expected, _decode_fingerprint(arg, True))
+
+ # checks with some malformed inputs
+ for arg in ('', '20wYcb', '20wYcb' * 30):
+ self.assertRaises(ValueError, _decode_fingerprint, arg, True)
+ self.assertEqual(None, _decode_fingerprint(arg, False))
+
+ def test_minimal(self):
+ """
+ Parses a minimal router status entry.
+ """
+
+ entry = RouterStatusEntry(get_router_status_entry(), None)
+
+ expected_flags = set([Flag.FAST, Flag.NAMED, Flag.RUNNING, Flag.STABLE, Flag.VALID])
+ self.assertEqual(None, entry.document)
+ self.assertEqual("caerSidi", entry.nickname)
+ self.assertEqual("A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB", entry.fingerprint)
+ self.assertEqual("oQZFLYe9e4A7bOkWKR7TaNxb0JE", entry.digest)
+ self.assertEqual(datetime.datetime(2012, 8, 6, 11, 19, 31), entry.published)
+ self.assertEqual("71.35.150.29", entry.address)
+ self.assertEqual(9001, entry.or_port)
+ self.assertEqual(None, entry.dir_port)
+ self.assertEqual(expected_flags, set(entry.flags))
+ self.assertEqual(None, entry.version_line)
+ self.assertEqual(None, entry.version)
+ self.assertEqual(None, entry.bandwidth)
+ 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.get_unrecognized_lines())
+
+ def test_missing_fields(self):
+ """
+ Parses a router status entry that's missing fields.
+ """
+
+ content = get_router_status_entry(exclude = ('r', 's'))
+ self._expect_invalid_attr(content, "address")
+
+ content = get_router_status_entry(exclude = ('r',))
+ self._expect_invalid_attr(content, "address")
+
+ content = get_router_status_entry(exclude = ('s',))
+ self._expect_invalid_attr(content, "flags")
+
+ def test_unrecognized_lines(self):
+ """
+ Parses a router status entry with new keywords.
+ """
+
+ content = get_router_status_entry({'z': 'New tor feature: sparkly unicorns!'})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(['z New tor feature: sparkly unicorns!'], entry.get_unrecognized_lines())
+
+ def test_proceeding_line(self):
+ """
+ Includes content prior to the 'r' line.
+ """
+
+ content = 'z some stuff\n' + get_router_status_entry()
+ self._expect_invalid_attr(content, "_unrecognized_lines", ['z some stuff'])
+
+ def test_blank_lines(self):
+ """
+ Includes blank lines, which should be ignored.
+ """
+
+ content = get_router_status_entry() + "\n\nv Tor 0.2.2.35\n\n"
+ entry = RouterStatusEntry(content, None)
+ self.assertEqual("Tor 0.2.2.35", entry.version_line)
+
+ def test_missing_r_field(self):
+ """
+ Excludes fields from the 'r' line.
+ """
+
+ components = (
+ ('nickname', 'caerSidi'),
+ ('fingerprint', 'p1aag7VwarGxqctS7/fS0y5FU+s'),
+ ('digest', 'oQZFLYe9e4A7bOkWKR7TaNxb0JE'),
+ ('published', '2012-08-06 11:19:31'),
+ ('address', '71.35.150.29'),
+ ('or_port', '9001'),
+ ('dir_port', '0'),
+ )
+
+ for attr, value in components:
+ # construct the 'r' line without this field
+ test_components = [comp[1] for comp in components]
+ test_components.remove(value)
+ r_line = ' '.join(test_components)
+
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_attr(content, attr)
+
+ def test_malformed_nickname(self):
+ """
+ Parses an 'r' line with a malformed nickname.
+ """
+
+ test_values = (
+ "",
+ "saberrider2008ReallyLongNickname", # too long
+ "$aberrider2008", # invalid characters
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("caerSidi", value)
+ content = get_router_status_entry({'r': r_line})
+
+ # TODO: Initial whitespace is consumed as part of the keyword/value
+ # divider. This is a bug in the case of V3 router status entries, but
+ # proper behavior for V2 router status entries and server/extrainfo
+ # descriptors.
+ #
+ # I'm inclined to leave this as-is for the moment since fixing it
+ # requires special KEYWORD_LINE handling, and the only result of this bug
+ # is that our validation doesn't catch the new SP restriction on V3
+ # entries.
+
+ if value == "": value = None
+
+ self._expect_invalid_attr(content, "nickname", value)
+
+ def test_malformed_fingerprint(self):
+ """
+ Parses an 'r' line with a malformed fingerprint.
+ """
+
+ test_values = (
+ "",
+ "zzzzz",
+ "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("p1aag7VwarGxqctS7/fS0y5FU+s", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_attr(content, "fingerprint")
+
+ def test_malformed_published_date(self):
+ """
+ Parses an 'r' line with a malformed published date.
+ """
+
+ test_values = (
+ "",
+ "2012-08-06 11:19:",
+ "2012-08-06 11:19:71",
+ "2012-08-06 11::31",
+ "2012-08-06 11:79:31",
+ "2012-08-06 :19:31",
+ "2012-08-06 41:19:31",
+ "2012-08- 11:19:31",
+ "2012-08-86 11:19:31",
+ "2012--06 11:19:31",
+ "2012-38-06 11:19:31",
+ "-08-06 11:19:31",
+ "2012-08-06 11:19:31",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("2012-08-06 11:19:31", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_attr(content, "published")
+
+ def test_malformed_address(self):
+ """
+ Parses an 'r' line with a malformed address.
+ """
+
+ test_values = (
+ "",
+ "71.35.150.",
+ "71.35..29",
+ "71.35.150",
+ "71.35.150.256",
+ )
+
+ for value in test_values:
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1].replace("71.35.150.29", value)
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_attr(content, "address", value)
+
+ def test_malformed_port(self):
+ """
+ Parses an 'r' line with a malformed ORPort or DirPort.
+ """
+
+ test_values = (
+ "",
+ "-1",
+ "399482",
+ "blarg",
+ )
+
+ for value in test_values:
+ for include_or_port in (False, True):
+ for include_dir_port in (False, True):
+ if not include_or_port and not include_dir_port:
+ continue
+
+ r_line = ROUTER_STATUS_ENTRY_ATTR[0][1]
+
+ if include_or_port:
+ r_line = r_line.replace(" 9001 ", " %s " % value)
+
+ if include_dir_port:
+ r_line = r_line[:-1] + value
+
+ attr = "or_port" if include_or_port else "dir_port"
+ expected = int(value) if value.isdigit() else None
+
+ content = get_router_status_entry({'r': r_line})
+ self._expect_invalid_attr(content, attr, expected)
+
+ def test_flags(self):
+ """
+ Handles a variety of flag inputs.
+ """
+
+ test_values = {
+ "": [],
+ "Fast": [Flag.FAST],
+ "Fast Valid": [Flag.FAST, Flag.VALID],
+ "Ugabuga": ["Ugabuga"],
+ }
+
+ for s_line, expected in test_values.items():
+ content = get_router_status_entry({'s': s_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.flags)
+
+ # tries some invalid inputs
+ test_values = {
+ "Fast ": [Flag.FAST, "", "", ""],
+ "Fast Valid": [Flag.FAST, "", Flag.VALID],
+ "Fast Fast": [Flag.FAST, Flag.FAST],
+ }
+
+ for s_line, expected in test_values.items():
+ content = get_router_status_entry({'s': s_line})
+ self._expect_invalid_attr(content, "flags", expected)
+
+ def test_versions(self):
+ """
+ Handles a variety of version inputs.
+ """
+
+ test_values = {
+ "Tor 0.2.2.35": Version("0.2.2.35"),
+ "Tor 0.1.2": Version("0.1.2"),
+ "Torr new_stuff": None,
+ "new_stuff and stuff": None,
+ }
+
+ for v_line, expected in test_values.items():
+ content = get_router_status_entry({'v': v_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.version)
+ self.assertEquals(v_line, entry.version_line)
+
+ # tries an invalid input
+ content = get_router_status_entry({'v': "Tor ugabuga"})
+ self._expect_invalid_attr(content, "version")
+
+ def test_bandwidth(self):
+ """
+ Handles a variety of 'w' lines.
+ """
+
+ test_values = {
+ "Bandwidth=0": (0, None, []),
+ "Bandwidth=63138": (63138, None, []),
+ "Bandwidth=11111 Measured=482": (11111, 482, []),
+ "Bandwidth=11111 Measured=482 Blarg!": (11111, 482, ["Blarg!"]),
+ }
+
+ for w_line, expected in test_values.items():
+ content = get_router_status_entry({'w': w_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected[0], entry.bandwidth)
+ self.assertEquals(expected[1], entry.measured)
+ self.assertEquals(expected[2], entry.unrecognized_bandwidth_entries)
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "blarg",
+ "Bandwidth",
+ "Bandwidth=",
+ "Bandwidth:0",
+ "Bandwidth 0",
+ "Bandwidth=-10",
+ "Bandwidth=10 Measured",
+ "Bandwidth=10 Measured=",
+ "Bandwidth=10 Measured=-50",
+ )
+
+ for w_line in test_values:
+ content = get_router_status_entry({'w': w_line})
+ self._expect_invalid_attr(content)
+
+ def test_exit_policy(self):
+ """
+ Handles a variety of 'p' lines.
+ """
+
+ test_values = {
+ "reject 1-65535": MicrodescriptorExitPolicy("reject 1-65535"),
+ "accept 80,110,143,443": MicrodescriptorExitPolicy("accept 80,110,143,443"),
+ }
+
+ for p_line, expected in test_values.items():
+ content = get_router_status_entry({'p': p_line})
+ entry = RouterStatusEntry(content, None)
+ self.assertEquals(expected, entry.exit_policy)
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "blarg",
+ "reject -50",
+ "accept 80,",
+ )
+
+ for p_line in test_values:
+ content = get_router_status_entry({'p': p_line})
+ self._expect_invalid_attr(content, "exit_policy")
+
+ def test_microdescriptor_hashes(self):
+ """
+ Handles a variety of 'm' lines.
+ """
+
+ test_values = {
+ "8,9,10,11,12":
+ [([8, 9, 10, 11, 12], {})],
+ "8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs":
+ [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
+ "8,9,10,11,12 sha256=g1vx9si329muxV md5=3tquWIXXySNOIwRGMeAESKs/v4DWs":
+ [([8, 9, 10, 11, 12], {"sha256": "g1vx9si329muxV", "md5": "3tquWIXXySNOIwRGMeAESKs/v4DWs"})],
+ }
+
+ # we need a document that's a vote
+ mock_document = lambda x: x # just need anything with a __dict__
+ mock_document.__dict__["vote_status"] = "vote"
+
+ for m_line, expected in test_values.items():
+ content = get_router_status_entry({'m': m_line})
+ entry = RouterStatusEntry(content, mock_document)
+ self.assertEquals(expected, entry.microdescriptor_hashes)
+
+ # try without a document
+ content = get_router_status_entry({'m': "8,9,10,11,12"})
+ self._expect_invalid_attr(content, "microdescriptor_hashes")
+
+ # tries some invalid inputs
+ test_values = (
+ "",
+ "4,a,2",
+ "1,2,3 stuff",
+ )
+
+ for m_line in test_values:
+ content = get_router_status_entry({'m': m_line})
+ self.assertRaises(ValueError, RouterStatusEntry, content, mock_document)
+
+ def _expect_invalid_attr(self, content, attr = None, expected_value = None):
+ """
+ Asserts that construction will fail due to content having a malformed
+ attribute. If an attr is provided then we check that it matches an expected
+ value when we're constructed without validation.
+ """
+
+ self.assertRaises(ValueError, RouterStatusEntry, content, None)
+ entry = RouterStatusEntry(content, None, False)
+
+ if attr:
+ self.assertEquals(expected_value, getattr(entry, attr))
+ else:
+ self.assertEquals("caerSidi", entry.nickname)
+
1
0
commit b931e980b62600d5cc9cd60069102c2ffa26cb84
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sat Sep 8 12:35:01 2012 -0700
Parsing vote-status attribute
Changing our 'vote_status' string attribute to 'is_vote' and 'is_consensus'
boolean attributes. The spec specifically says that anything else is invalid so
there's little reason to allow arbitrary content in the field.
---
stem/descriptor/networkstatus.py | 61 ++++++++++++------------
test/integ/descriptor/networkstatus.py | 6 ++-
test/unit/descriptor/networkstatus/document.py | 27 +++++++++-
test/unit/descriptor/networkstatus/entry.py | 3 +-
4 files changed, 60 insertions(+), 37 deletions(-)
diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
index d554d42..f4c3b40 100644
--- a/stem/descriptor/networkstatus.py
+++ b/stem/descriptor/networkstatus.py
@@ -199,7 +199,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
:var tuple routers: RouterStatusEntry contained in the document
:var str version: **\*** document version
- :var str vote_status: **\*** status of the vote (is either "vote" or "consensus")
+ :var bool is_consensus: **\*** true if the document is a consensus
+ :var bool is_vote: **\*** true if the document is a vote
:var int consensus_method: **~** consensus method used to generate a consensus
:var list consensus_methods: **^** A list of supported consensus generation methods (integers)
:var datetime published: **^** time when the document was published
@@ -237,7 +238,8 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
self.directory_signatures = []
self.version = None
- self.vote_status = None
+ self.is_consensus = True
+ self.is_vote = False
self.consensus_methods = []
self.published = None
self.consensus_method = None
@@ -293,19 +295,6 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
all_entries.update(header_entries)
all_entries.update(footer_entries)
- if 'vote-status' in header_entries:
- is_consensus = header_entries['vote-status'][0][0] == "consensus"
- is_vote = not is_consensus
- else:
- if validate:
- raise ValueError("Network status documents must have a 'vote-status' line to say if they're a vote or consensus")
-
- is_consensus, is_vote = True, False
-
- if validate:
- self._check_for_missing_and_disallowed_fields(is_consensus, header_entries, footer_entries)
- self._check_for_misordered_fields(is_consensus, header_entries, footer_entries)
-
known_fields = [attr[0] for attr in HEADER_STATUS_DOCUMENT_FIELDS + FOOTER_STATUS_DOCUMENT_FIELDS]
content = header + '\n' + footer
@@ -330,6 +319,22 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
if validate and self.version != "3":
raise ValueError("Expected a version 3 network status documents, got version '%s' instead" % self.version)
+ elif keyword == 'vote-status':
+ # "vote-status" type
+
+ if value == 'consensus':
+ self.is_consensus, self.is_vote = True, False
+ elif value == 'vote':
+ self.is_consensus, self.is_vote = False, True
+ elif validate:
+ raise ValueError("A network status document's vote-status line can only be 'consensus' or 'vote', got '%s' instead" % value)
+
+ # doing this validation afterward so we know our 'is_consensus' and
+ # 'is_vote' attributes
+
+ if validate:
+ self._check_for_missing_and_disallowed_fields(header_entries, footer_entries)
+ self._check_for_misordered_fields(header_entries, footer_entries)
def _parse_old(self, raw_content, validate):
# preamble
@@ -338,14 +343,9 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
# ignore things the parse() method handles
_read_keyword_line("network-status-version", content, False, True)
+ _read_keyword_line("vote-status", content, False, True)
-
- map(read_keyword_line, ["vote-status"])
-
- vote = False
- if self.vote_status == "vote": vote = True
- elif self.vote_status == "consensus": vote = False
- elif validate: raise ValueError("Unrecognized vote-status")
+ vote = self.is_vote
if vote:
read_keyword_line("consensus-methods", True)
@@ -431,30 +431,28 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
else:
self.unrecognized_lines = []
- def _check_for_missing_and_disallowed_fields(self, is_consensus, header_entries, footer_entries):
+ def _check_for_missing_and_disallowed_fields(self, header_entries, footer_entries):
"""
Checks that we have mandatory fields for our type, and that we don't have
any fields exclusive to the other (ie, no vote-only fields appear in a
consensus or vice versa).
- :param bool is_consensus: true if we're a conensus and false if we're a vote
:param dict header_entries: ordered keyword/value mappings of the header
:param dict footer_entries: ordered keyword/value mappings of the footer
:raises: ValueError if we're missing mandatory fields or have fiels we shouldn't
"""
- is_vote = not is_consensus # aliasing inverse for readability
missing_fields, disallowed_fields = [], []
for entries, fields in ((header_entries, HEADER_STATUS_DOCUMENT_FIELDS),\
(footer_entries, FOOTER_STATUS_DOCUMENT_FIELDS)):
for field, in_votes, in_consensus, mandatory in fields:
- if mandatory and ((is_consensus and in_consensus) or (is_vote and in_votes)):
+ if mandatory and ((self.is_consensus and in_consensus) or (self.is_vote and in_votes)):
# mandatory field, check that we have it
if not field in entries.keys():
missing_fields.append(field)
- elif (is_consensus and not in_consensus) or (is_vote and not in_votes):
+ elif (self.is_consensus and not in_consensus) or (self.is_vote and not in_votes):
# field we shouldn't have, check that we don't
if field in entries.keys():
disallowed_fields.append(field)
@@ -465,7 +463,7 @@ class NetworkStatusDocument(stem.descriptor.Descriptor):
if disallowed_fields:
raise ValueError("Network status document has fields that shouldn't appear in this document type: %s" % ', '.join(disallowed_fields))
- def _check_for_misordered_fields(self, is_consensus, header_entries, footer_entries):
+ def _check_for_misordered_fields(self, header_entries, footer_entries):
"""
To be valid a network status document's fiends need to appear in a specific
order. Checks that known fields appear in that order (unrecognized fields
@@ -824,10 +822,10 @@ class RouterStatusEntry(stem.descriptor.Descriptor):
m_comp = value.split(" ")
- if not (self.document and self.document.vote_status == "vote"):
+ if not (self.document and self.document.is_vote):
if not validate: continue
- vote_status = self.document.vote_status if self.document else "<undefined document>"
+ vote_status = "vote" if self.document else "<undefined document>"
raise ValueError("Router status entry's 'm' line should only appear in votes (appeared in a %s): %s" % (vote_status, line))
elif len(m_comp) < 1:
if not validate: continue
@@ -870,7 +868,8 @@ class MicrodescriptorConsensus(NetworkStatusDocument):
A v3 microdescriptor consensus.
:var str version: **\*** a document format version. For v3 microdescriptor consensuses this is "3 microdesc"
- :var str vote_status: **\*** status of the vote (is "consensus")
+ :var bool is_consensus: **\*** true if the document is a consensus
+ :var bool is_vote: **\*** true if the document is a vote
:var int consensus_method: **~** consensus method used to generate a consensus
:var datetime valid_after: **\*** time when the consensus becomes valid
:var datetime fresh_until: **\*** time until when the consensus is considered to be fresh
diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py
index 1777828..9087b22 100644
--- a/test/integ/descriptor/networkstatus.py
+++ b/test/integ/descriptor/networkstatus.py
@@ -86,7 +86,8 @@ class TestNetworkStatus(unittest.TestCase):
descriptor_file.close()
self.assertEquals("3", desc.version)
- self.assertEquals("consensus", desc.vote_status)
+ self.assertEquals(True, desc.is_consensus)
+ self.assertEquals(False, desc.is_vote)
self.assertEquals([], desc.consensus_methods)
self.assertEquals(None, desc.published)
self.assertEquals(12, desc.consensus_method)
@@ -178,7 +179,8 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY=
descriptor_file.close()
self.assertEquals("3", desc.version)
- self.assertEquals("vote", desc.vote_status)
+ self.assertEquals(False, desc.is_consensus)
+ self.assertEquals(True, desc.is_vote)
self.assertEquals(range(1, 13), desc.consensus_methods)
self.assertEquals(_strptime("2012-07-11 23:50:01"), desc.published)
self.assertEquals(None, desc.consensus_method)
diff --git a/test/unit/descriptor/networkstatus/document.py b/test/unit/descriptor/networkstatus/document.py
index a099fa9..9b24a00 100644
--- a/test/unit/descriptor/networkstatus/document.py
+++ b/test/unit/descriptor/networkstatus/document.py
@@ -101,7 +101,8 @@ class TestNetworkStatusDocument(unittest.TestCase):
self.assertEqual((), document.routers)
self.assertEqual("3", document.version)
- self.assertEqual("consensus", document.vote_status)
+ self.assertEqual(True, document.is_consensus)
+ self.assertEqual(False, document.is_vote)
self.assertEqual(9, document.consensus_method)
self.assertEqual([], document.consensus_methods)
self.assertEqual(None, document.published)
@@ -134,7 +135,8 @@ class TestNetworkStatusDocument(unittest.TestCase):
self.assertEqual((), document.routers)
self.assertEqual("3", document.version)
- self.assertEqual("vote", document.vote_status)
+ self.assertEqual(False, document.is_consensus)
+ self.assertEqual(True, document.is_vote)
self.assertEqual(None, document.consensus_method)
self.assertEqual([9], document.consensus_methods)
self.assertEqual(datetime.datetime(2012, 9, 2, 22, 0, 0), document.published)
@@ -216,7 +218,7 @@ class TestNetworkStatusDocument(unittest.TestCase):
def test_invalid_version(self):
"""
- Try parsing a different document version with the v3 parser.
+ Parses a different document version with the v3 parser.
"""
content = get_network_status_document({"network-status-version": "4"})
@@ -224,4 +226,23 @@ class TestNetworkStatusDocument(unittest.TestCase):
document = NetworkStatusDocument(content, False)
self.assertEquals("4", document.version)
+
+ def test_invalid_vote_status(self):
+ """
+ Parses an invalid vote-status field.
+ """
+
+ test_values = (
+ "",
+ " ",
+ "votee",
+ )
+
+ for test_value in test_values:
+ content = get_network_status_document({"vote-status": test_value})
+ self.assertRaises(ValueError, NetworkStatusDocument, content)
+
+ document = NetworkStatusDocument(content, False)
+ self.assertEquals(True, document.is_consensus)
+ self.assertEquals(False, document.is_vote)
diff --git a/test/unit/descriptor/networkstatus/entry.py b/test/unit/descriptor/networkstatus/entry.py
index 7eb0cb8..195600c 100644
--- a/test/unit/descriptor/networkstatus/entry.py
+++ b/test/unit/descriptor/networkstatus/entry.py
@@ -408,7 +408,8 @@ class TestRouterStatusEntry(unittest.TestCase):
# we need a document that's a vote
mock_document = lambda x: x # just need anything with a __dict__
- mock_document.__dict__["vote_status"] = "vote"
+ mock_document.__dict__["is_vote"] = True
+ mock_document.__dict__["is_consensus"] = False
for m_line, expected in test_values.items():
content = get_router_status_entry({'m': m_line})
1
0