commit 372ee9836b98af582eecf3d35844397d8935bd9b Author: Damian Johnson atagar@torproject.org Date: Thu Oct 11 19:27:36 2012 -0700
Parsing and tests for network status document v2
Parser, unit, and integ test for version 2 network status documents. These documents are deprecated and no longer generated, however we still need a parser to read older consensuses.
Unlike the v3 parser I'm cutting a few corners...
- not validating parameter ordering - no validation that header/footer parameters haven't swapped places - only the bare minimum unit test, no tests for invalid content
We can remedy these if necessary but with the growing irrelevance of v2 consensus parsing I doubt we ever will. Plenty of more important things to do. --- run_tests.py | 2 + stem/descriptor/networkstatus.py | 207 ++++++++++++++++++++- test/integ/descriptor/data/cached-consensus-v2 | 27 +++ test/integ/descriptor/networkstatus.py | 80 ++++++++- test/mocking.py | 34 ++++ test/unit/descriptor/networkstatus/document_v2.py | 32 ++++ 6 files changed, 371 insertions(+), 11 deletions(-)
diff --git a/run_tests.py b/run_tests.py index 2475254..ef172bf 100755 --- a/run_tests.py +++ b/run_tests.py @@ -23,6 +23,7 @@ import test.unit.descriptor.extrainfo_descriptor import test.unit.descriptor.router_status_entry import test.unit.descriptor.networkstatus.directory_authority import test.unit.descriptor.networkstatus.key_certificate +import test.unit.descriptor.networkstatus.document_v2 import test.unit.descriptor.networkstatus.document_v3 import test.unit.response.control_line import test.unit.response.control_message @@ -121,6 +122,7 @@ UNIT_TESTS = ( test.unit.descriptor.router_status_entry.TestRouterStatusEntry, test.unit.descriptor.networkstatus.directory_authority.TestDirectoryAuthority, test.unit.descriptor.networkstatus.key_certificate.TestKeyCertificate, + test.unit.descriptor.networkstatus.document_v2.TestNetworkStatusDocument, test.unit.descriptor.networkstatus.document_v3.TestNetworkStatusDocument, test.unit.exit_policy.rule.TestExitPolicyRule, test.unit.exit_policy.policy.TestExitPolicy, diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py index 2f6972f..a028572 100644 --- a/stem/descriptor/networkstatus.py +++ b/stem/descriptor/networkstatus.py @@ -58,19 +58,39 @@ routers. Those routers refer to a 'thin' document, which doesn't have a ::
parse_file - parses a network status file, providing an iterator for its routers - NetworkStatusDocumentV3 - Version 3 network status document. + + NetworkStatusDocument - Network status document. + |- NetworkStatusDocumentV2 - Version 2 network status document. + +- NetworkStatusDocumentV3 - Version 3 network status document. + DocumentSignature - Signature of a document by a directory authority. DirectoryAuthority - Directory authority as defined in a v3 network status document. """
import datetime -from StringIO import StringIO +import StringIO
import stem.descriptor import stem.descriptor.router_status_entry import stem.version import stem.util.tor_tools
+# Version 2 network status document fields, tuples of the form... +# (keyword, is_mandatory) + +NETWORK_STATUS_V2_FIELDS = ( + ("network-status-version", True), + ("dir-source", True), + ("fingerprint", True), + ("contact", True), + ("dir-signing-key", True), + ("client-versions", False), + ("server-versions", False), + ("published", True), + ("dir-options", False), + ("directory-signature", True), +) + # 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... @@ -105,6 +125,7 @@ FOOTER_FIELDS = [attr[0] for attr in FOOTER_STATUS_DOCUMENT_FIELDS] AUTH_START = "dir-source" ROUTERS_START = "r" FOOTER_START = "directory-footer" +V2_FOOTER_START = "directory-signature"
DEFAULT_PARAMS = { "bwweightscale": 10000, @@ -229,7 +250,179 @@ def _get_entries(document_file, validate, entry_class, entry_keyword, start_posi desc_content = "".join(stem.descriptor._read_until_keywords(entry_keyword, document_file, ignore_first = True, end_position = end_position)) yield entry_class(desc_content, validate, *extra_args)
-class NetworkStatusDocumentV3(stem.descriptor.Descriptor): +class NetworkStatusDocument(stem.descriptor.Descriptor): + """ + Common parent for network status documents. + """ + + def __init__(self, raw_content): + super(NetworkStatusDocument, self).__init__(raw_content) + self._unrecognized_lines = [] + + def get_unrecognized_lines(self): + return list(self._unrecognized_lines) + +class NetworkStatusDocumentV2(NetworkStatusDocument): + """ + Version 2 network status document. These have been deprecated and are no + longer generated by Tor. + + :var tuple routers: RouterStatusEntryV2 contained in the document + + :var int version: ***** document version + + :var str hostname: ***** hostname of the authority + :var str address: ***** authority's IP address + :var int dir_port: ***** authority's DirPort + :var str fingerprint: ***** authority's fingerprint + :var str contact: ***** authority's contact information + :var str signing_key: ***** authority's public signing key + + :var list client_versions: list of recommended client tor version strings + :var list server_versions: list of recommended server tor version strings + :var datetime published: ***** time when the document was published + :var list options: ***** list of things that this authority decides + + :var str signing_authority: ***** name of the authority signing the document + :var str signature: ***** authority's signature for the document + + ***** 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_content, validate = True): + super(NetworkStatusDocumentV2, self).__init__(raw_content) + + self.version = None + self.hostname = None + self.address = None + self.dir_port = None + self.fingerprint = None + self.contact = None + self.signing_key = None + + self.client_versions = [] + self.server_versions = [] + self.published = None + self.options = [] + + self.signing_authority = None + self.signatures = None + + # Splitting the document from the routers. Unlike v3 documents we're not + # bending over backwards on the validation by checking the field order or + # that header/footer attributes aren't in the wrong section. This is a + # deprecated descriptor type - patches welcome if you want those checks. + + document_file = StringIO.StringIO(raw_content) + document_content = "".join(stem.descriptor._read_until_keywords((ROUTERS_START, V2_FOOTER_START), document_file)) + + self.routers = tuple(_get_entries( + document_file, + validate, + entry_class = stem.descriptor.router_status_entry.RouterStatusEntryV2, + entry_keyword = ROUTERS_START, + section_end_keywords = V2_FOOTER_START, + extra_args = (self,), + )) + + document_content += "\n" + document_file.read() + + entries = stem.descriptor._get_descriptor_components(document_content, validate) + if validate: self._check_constraints(entries) + self._parse(entries, validate) + + def _parse(self, entries, validate): + for keyword, values in entries.items(): + value, block_contents = values[0] + + line = "%s %s" % (keyword, value) # original line + if block_contents: line += "\n%s" % block_contents + + if keyword == "network-status-version": + if not value.isdigit(): + if not validate: continue + raise ValueError("Network status document has a non-numeric version: %s" % line) + + self.version = int(value) + + if validate and self.version != 2: + raise ValueError("Expected a version 2 network status document, got version '%s' instead" % self.version) + elif keyword == "dir-source": + dir_source_comp = value.split() + + if len(dir_source_comp) < 3: + if not validate: continue + raise ValueError("The 'dir-source' line of a v2 network status document must have three values: %s" % line) + + if validate: + if not dir_source_comp[0]: + # https://trac.torproject.org/7055 + raise ValueError("Authority's hostname can't be blank: %s" % line) + elif not stem.util.connection.is_valid_ip_address(dir_source_comp[1]): + raise ValueError("Authority's address isn't a valid IPv4 address: %s" % dir_source_comp[1]) + elif not stem.util.connection.is_valid_port(dir_source_comp[2], allow_zero = True): + raise ValueError("Authority's DirPort is invalid: %s" % dir_source_comp[2]) + elif not dir_source_comp[2].isdigit(): + continue + + self.hostname = dir_source_comp[0] + self.address = dir_source_comp[1] + self.dir_port = None if dir_source_comp[2] == '0' else int(dir_source_comp[2]) + elif keyword == "fingerprint": + if validate and not stem.util.tor_tools.is_valid_fingerprint(value): + raise ValueError("Authority's fingerprint in a v2 network status document is malformed: %s" % line) + + self.fingerprint = value + elif keyword == "contact": + self.contact = value + elif keyword == "dir-signing-key": + self.signing_key = block_contents + elif keyword in ("client-versions", "server-versions"): + # v2 documents existed while there were tor versions using the 'old' + # style, hence we aren't attempting to parse them + + for version_str in value.split(","): + if keyword == 'client-versions': + self.client_versions.append(version_str) + elif keyword == 'server-versions': + self.server_versions.append(version_str) + elif keyword == "published": + try: + self.published = datetime.datetime.strptime(value, "%Y-%m-%d %H:%M:%S") + except ValueError: + if validate: + raise ValueError("Versino 2 network status document's 'published' time wasn't parseable: %s" % value) + elif keyword == "dir-options": + self.options = value.split() + elif keyword == "directory-signature": + self.signing_authority = value + self.signature = block_contents + else: + self._unrecognized_lines.append(line) + + # 'client-versions' and 'server-versions' are only required if "Versions" + # is among the options + + if validate and "Versions" in self.options: + if not ('client-versions' in entries and 'server-versions' in entries): + raise ValueError("Version 2 network status documents must have a 'client-versions' and 'server-versions' when 'Versions' is listed among its dir-options:\n%s" % str(self)) + + def _check_constraints(self, entries): + required_fields = [field for (field, is_mandatory) in NETWORK_STATUS_V2_FIELDS if is_mandatory] + for keyword in required_fields: + if not keyword in entries: + raise ValueError("Network status document (v2) must have a '%s' line:\n%s" % (keyword, str(self))) + + # all recognized fields can only appear once + single_fields = [field for (field, _) in NETWORK_STATUS_V2_FIELDS] + for keyword in single_fields: + if keyword in entries and len(entries[keyword]) > 1: + raise ValueError("Network status document (v2) can only have a single '%s' line, got %i:\n%s" % (keyword, len(entries[keyword]), str(self))) + + if 'network-status-version' != entries.keys()[0]: + raise ValueError("Network status document (v2) are expected to start with a 'network-status-version' line:\n%s" % str(self)) + +class NetworkStatusDocumentV3(NetworkStatusDocument): """ Version 3 network status document. This could be either a vote or consensus.
@@ -276,10 +469,9 @@ class NetworkStatusDocumentV3(stem.descriptor.Descriptor): """
super(NetworkStatusDocumentV3, self).__init__(raw_content) - document_file = StringIO(raw_content) + document_file = StringIO.StringIO(raw_content)
self._header = _DocumentHeader(document_file, validate, default_params) - self._unrecognized_lines = []
# merge header attributes into us for attr, value in vars(self._header).items(): @@ -333,9 +525,6 @@ class NetworkStatusDocumentV3(stem.descriptor.Descriptor):
return self._header.meets_consensus_method(method)
- def get_unrecognized_lines(self): - return list(self._unrecognized_lines) - def __cmp__(self, other): if not isinstance(other, NetworkStatusDocumentV3): return 1 @@ -404,7 +593,7 @@ class _DocumentHeader(object): self.is_microdescriptor = flavor == 'microdesc'
if validate and self.version != 3: - raise ValueError("Expected a version 3 network status documents, got version '%s' instead" % self.version) + raise ValueError("Expected a version 3 network status document, got version '%s' instead" % self.version) elif keyword == 'vote-status': # "vote-status" type # diff --git a/test/integ/descriptor/data/cached-consensus-v2 b/test/integ/descriptor/data/cached-consensus-v2 new file mode 100644 index 0000000..4f8cbce --- /dev/null +++ b/test/integ/descriptor/data/cached-consensus-v2 @@ -0,0 +1,27 @@ +network-status-version 2 +dir-source 18.244.0.114 18.244.0.114 80 +fingerprint 719BE45DE224B607C53707D0E2143E2D423E74CF +contact arma at mit dot edu +published 2005-12-16 00:13:46 +dir-options Names Versions +client-versions 0.0.9rc2,0.0.9rc3,0.0.9rc4-cvs,0.0.9rc4,0.0.9rc5-cvs,0.0.9rc5,0.0.9rc6-cvs,0.0.9rc6,0.0.9rc7-cvs,0.0.9rc7,0.0.9,0.0.9.1,0.0.9.2,0.0.9.3,0.0.9.4,0.0.9.5,0.0.9.6,0.0.9.7,0.0.9.8,0.0.9.9,0.0.9.10,0.1.0.0-alpha-cvs,0.1.0.1-rc,0.1.0.1-rc-cvs,0.1.0.2-rc,0.1.0.2-rc-cvs,0.1.0.3-rc,0.1.0.3-rc-cvs,0.1.0.4-rc,0.1.0.4-rc-cvs,0.1.0.5-rc,0.1.0.5-rc-cvs,0.1.0.6-rc,0.1.0.6-rc-cvs,0.1.0.7-rc,0.1.0.7-rc-cvs,0.1.0.8-rc,0.1.0.8-rc-cvs,0.1.0.9-rc,0.1.0.10,0.1.0.11,0.1.0.12,0.1.0.13,0.1.0.14,0.1.0.15,0.1.0.16,0.1.1.0-alpha-cvs,0.1.1.1-alpha,0.1.1.1-alpha-cvs,0.1.1.2-alpha,0.1.1.2-alpha-cvs,0.1.1.3-alpha,0.1.1.3-alpha-cvs,0.1.1.4-alpha,0.1.1.4-alpha-cvs,0.1.1.5-alpha,0.1.1.5-alpha-cvs,0.1.1.6-alpha,0.1.1.6-alpha-cvs,0.1.1.7-alpha,0.1.1.7-alpha-cvs,0.1.1.8-alpha,0.1.1.8-alpha-cvs,0.1.1.9-alpha,0.1.1.9-alpha-cvs,0.1.1.10-alpha,0.1.1.10-alpha-cvs +server-versions 0.0.9rc2,0.0.9rc3,0.0.9rc4-cvs,0.0.9rc4,0.0.9rc5-cvs,0.0.9rc5,0.0.9rc6-cvs,0.0.9rc6,0.0.9rc7-cvs,0.0.9rc7,0.0.9,0.0.9.1,0.0.9.2,0.0.9.3,0.0.9.4,0.0.9.5,0.0.9.6,0.0.9.7,0.0.9.8,0.0.9.9,0.0.9.10,0.1.0.0-alpha-cvs,0.1.0.1-rc,0.1.0.1-rc-cvs,0.1.0.2-rc,0.1.0.2-rc-cvs,0.1.0.3-rc,0.1.0.3-rc-cvs,0.1.0.4-rc,0.1.0.4-rc-cvs,0.1.0.5-rc,0.1.0.5-rc-cvs,0.1.0.6-rc,0.1.0.6-rc-cvs,0.1.0.7-rc,0.1.0.7-rc-cvs,0.1.0.8-rc,0.1.0.8-rc-cvs,0.1.0.9-rc,0.1.0.10,0.1.0.11,0.1.0.12,0.1.0.13,0.1.0.14,0.1.0.15,0.1.0.16,0.1.1.0-alpha-cvs,0.1.1.1-alpha,0.1.1.1-alpha-cvs,0.1.1.2-alpha,0.1.1.2-alpha-cvs,0.1.1.3-alpha,0.1.1.3-alpha-cvs,0.1.1.4-alpha,0.1.1.4-alpha-cvs,0.1.1.5-alpha,0.1.1.5-alpha-cvs,0.1.1.6-alpha,0.1.1.6-alpha-cvs,0.1.1.7-alpha,0.1.1.7-alpha-cvs,0.1.1.8-alpha,0.1.1.8-alpha-cvs,0.1.1.9-alpha,0.1.1.9-alpha-cvs,0.1.1.10-alpha,0.1.1.10-alpha-cvs +dir-signing-key +-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAOcrht/y5rkaahfX7sMe2qnpqoPibsjTSJaDvsUtaNP/Bq0MgNDGOR48 +rtwfqTRff275Edkp/UYw3G3vSgKCJr76/bqOHCmkiZrnPV1zxNfrK18gNw2Cxre0 +nTA+fD8JQqpPtb8b0SnG9kwy75eS//sRu7TErie2PzGMxrf9LH0LAgMBAAE= +-----END RSA PUBLIC KEY----- + +r moria2 cZvkXeIktgfFNwfQ4hQ+LUI+dM8 t/Pwl1uHiJ3RKF/Vehsbthf2VDI 2005-12-15 06:57:18 18.244.0.114 443 80 +s Authority Fast Named Running Valid V2Dir +r stnv CSi6RnBWxKaJ/uTvXXFIK2KJw9U ItGn7UGZvaftbEFu7NdpwY4fKlo 2005-12-15 16:24:42 84.16.236.173 9001 0 +s Named Valid +r nggrplz CehYL/Dm+F4rjkHA3AucncRuaWg swLCsByU85jj7ziTlSawZR+CTdY 2005-12-15 23:25:50 194.109.109.109 9001 0 +s Fast Stable Running Valid +directory-signature moria2 +-----BEGIN SIGNATURE----- +2nXCxVje3wzn6HrIFRNMc0nc48AhMVpHZyPwRKGXkuYfTQG55uvwQDaFgJHud4RT +27QhWltau3K1evhnzhKcpbTXwkVv1TBYJSzL6rEeAn8cQ7ZiCyqf4EJCaNcem3d2 +TpQQk3nNQF8z6UIvdlvP+DnJV4izWVkQEZgUZgIVM0E= +-----END SIGNATURE----- diff --git a/test/integ/descriptor/networkstatus.py b/test/integ/descriptor/networkstatus.py index e174f25..e0ecd00 100644 --- a/test/integ/descriptor/networkstatus.py +++ b/test/integ/descriptor/networkstatus.py @@ -108,9 +108,9 @@ class TestNetworkStatus(unittest.TestCase): self.assertEquals(80, router.or_port) self.assertEquals(None, router.dir_port)
- def test_consensus(self): + def test_consensus_v3(self): """ - Checks that consensus documents are properly parsed. + Checks that version 3 consensus documents are properly parsed. """
# the document's expected client and server versions are the same @@ -193,6 +193,82 @@ I/TJmV928na7RLZe2mGHCAW3VQOvV+QkCfj05VZ8CsY= self.assertEquals("BF112F1C6D5543CFD0A32215ACABD4197B5279AD", signature.key_digest) self.assertEquals(expected_signature, signature.signature)
+ def test_consensus_v2(self): + """ + Checks that version 2 consensus documents are properly parsed. + """ + + expected_signing_key = """-----BEGIN RSA PUBLIC KEY----- +MIGJAoGBAOcrht/y5rkaahfX7sMe2qnpqoPibsjTSJaDvsUtaNP/Bq0MgNDGOR48 +rtwfqTRff275Edkp/UYw3G3vSgKCJr76/bqOHCmkiZrnPV1zxNfrK18gNw2Cxre0 +nTA+fD8JQqpPtb8b0SnG9kwy75eS//sRu7TErie2PzGMxrf9LH0LAgMBAAE= +-----END RSA PUBLIC KEY-----""" + + expected_signature = """-----BEGIN SIGNATURE----- +2nXCxVje3wzn6HrIFRNMc0nc48AhMVpHZyPwRKGXkuYfTQG55uvwQDaFgJHud4RT +27QhWltau3K1evhnzhKcpbTXwkVv1TBYJSzL6rEeAn8cQ7ZiCyqf4EJCaNcem3d2 +TpQQk3nNQF8z6UIvdlvP+DnJV4izWVkQEZgUZgIVM0E= +-----END SIGNATURE-----""" + + consensus_path = test.integ.descriptor.get_resource("cached-consensus-v2") + + with open(consensus_path) as descriptor_file: + document = stem.descriptor.networkstatus.NetworkStatusDocumentV2(descriptor_file.read()) + + self.assertEquals(2, document.version) + self.assertEquals("18.244.0.114", document.hostname) + self.assertEquals("18.244.0.114", document.address) + self.assertEquals(80, document.dir_port) + self.assertEquals("719BE45DE224B607C53707D0E2143E2D423E74CF", document.fingerprint) + self.assertEquals("arma at mit dot edu", document.contact) + self.assertEquals(expected_signing_key, document.signing_key) + + self.assertEquals(67, len(document.client_versions)) + self.assertEquals("0.0.9rc2", document.client_versions[0]) + self.assertEquals("0.1.1.10-alpha-cvs", document.client_versions[-1]) + + self.assertEquals(67, len(document.server_versions)) + self.assertEquals("0.0.9rc2", document.server_versions[0]) + self.assertEquals("0.1.1.10-alpha-cvs", document.server_versions[-1]) + + self.assertEquals(datetime.datetime(2005, 12, 16, 0, 13, 46), document.published) + self.assertEquals(["Names", "Versions"], document.options) + self.assertEquals("moria2", document.signing_authority) + self.assertEquals(expected_signature, document.signature) + self.assertEquals([], document.get_unrecognized_lines()) + + self.assertEqual(3, len(document.routers)) + + router1 = document.routers[0] + self.assertEquals("moria2", router1.nickname) + self.assertEquals("719BE45DE224B607C53707D0E2143E2D423E74CF", router1.fingerprint) + self.assertEquals("t/Pwl1uHiJ3RKF/Vehsbthf2VDI", router1.digest) + self.assertEquals(datetime.datetime(2005, 12, 15, 6, 57, 18), router1.published) + self.assertEquals("18.244.0.114", router1.address) + self.assertEquals(443, router1.or_port) + self.assertEquals(80, router1.dir_port) + self.assertEquals(set(["Authority", "Fast", "Named", "Running", "Valid", "V2Dir"]), set(router1.flags)) + + router2 = document.routers[1] + self.assertEquals("stnv", router2.nickname) + self.assertEquals("0928BA467056C4A689FEE4EF5D71482B6289C3D5", router2.fingerprint) + self.assertEquals("ItGn7UGZvaftbEFu7NdpwY4fKlo", router2.digest) + self.assertEquals(datetime.datetime(2005, 12, 15, 16, 24, 42), router2.published) + self.assertEquals("84.16.236.173", router2.address) + self.assertEquals(9001, router2.or_port) + self.assertEquals(None, router2.dir_port) + self.assertEquals(set(["Named", "Valid"]), set(router2.flags)) + + router3 = document.routers[2] + self.assertEquals("nggrplz", router3.nickname) + self.assertEquals("09E8582FF0E6F85E2B8E41C0DC0B9C9DC46E6968", router3.fingerprint) + self.assertEquals("swLCsByU85jj7ziTlSawZR+CTdY", router3.digest) + self.assertEquals(datetime.datetime(2005, 12, 15, 23, 25, 50), router3.published) + self.assertEquals("194.109.109.109", router3.address) + self.assertEquals(9001, router3.or_port) + self.assertEquals(None, router3.dir_port) + self.assertEquals(set(["Fast", "Stable", "Running", "Valid"]), set(router3.flags)) + def test_metrics_vote(self): """ Checks if vote documents from Metrics are parsed properly. diff --git a/test/mocking.py b/test/mocking.py index ab31eef..1a8aa41 100644 --- a/test/mocking.py +++ b/test/mocking.py @@ -36,6 +36,7 @@ calling :func:`test.mocking.revert_mocking`. stem.descriptor.networkstatus get_directory_authority - DirectoryAuthority get_key_certificate - KeyCertificate + get_network_status_document_v2 - NetworkStatusDocumentV2 get_network_status_document_v3 - NetworkStatusDocumentV3
stem.descriptor.router_status_entry @@ -152,6 +153,19 @@ KEY_CERTIFICATE_FOOTER = ( ("dir-key-certification", "\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----" % CRYPTO_BLOB), )
+NETWORK_STATUS_DOCUMENT_HEADER_V2 = ( + ("network-status-version", "2"), + ("dir-source", "18.244.0.114 18.244.0.114 80"), + ("fingerprint", "719BE45DE224B607C53707D0E2143E2D423E74CF"), + ("contact", "arma at mit dot edu"), + ("published", "2005-12-16 00:13:46"), + ("dir-signing-key", "\n-----BEGIN RSA PUBLIC KEY-----%s-----END RSA PUBLIC KEY-----" % CRYPTO_BLOB), +) + +NETWORK_STATUS_DOCUMENT_FOOTER_V2 = ( + ("directory-signature", "moria2\n-----BEGIN SIGNATURE-----%s-----END SIGNATURE-----" % CRYPTO_BLOB), +) + NETWORK_STATUS_DOCUMENT_HEADER = ( ("network-status-version", "3"), ("vote-status", "consensus"), @@ -655,6 +669,26 @@ def get_key_certificate(attr = None, exclude = (), content = False): else: return stem.descriptor.networkstatus.KeyCertificate(desc_content, validate = True)
+def get_network_status_document_v2(attr = None, exclude = (), routers = None, content = False): + """ + Provides the descriptor content for... + stem.descriptor.networkstatus.NetworkStatusDocumentV2 + + :param dict attr: keyword/value mappings to be included in the descriptor + :param list exclude: mandatory keywords to exclude from the descriptor + :param list routers: router status entries to include in the document + :param bool content: provides the str content of the descriptor rather than the class if True + + :returns: NetworkStatusDocumentV2 for the requested descriptor content + """ + + desc_content = _get_descriptor_content(attr, exclude, NETWORK_STATUS_DOCUMENT_HEADER_V2, NETWORK_STATUS_DOCUMENT_FOOTER_V2) + + if content: + return desc_content + else: + return stem.descriptor.networkstatus.NetworkStatusDocumentV2(desc_content, validate = True) + def get_network_status_document_v3(attr = None, exclude = (), authorities = None, routers = None, content = False): """ Provides the descriptor content for... diff --git a/test/unit/descriptor/networkstatus/document_v2.py b/test/unit/descriptor/networkstatus/document_v2.py new file mode 100644 index 0000000..70f904e --- /dev/null +++ b/test/unit/descriptor/networkstatus/document_v2.py @@ -0,0 +1,32 @@ +""" +Unit tests for the NetworkStatusDocumentV2 of stem.descriptor.networkstatus. +""" + +import datetime +import unittest + +from test.mocking import get_network_status_document_v2, NETWORK_STATUS_DOCUMENT_HEADER_V2, NETWORK_STATUS_DOCUMENT_FOOTER_V2 + +class TestNetworkStatusDocument(unittest.TestCase): + def test_minimal_document(self): + """ + Parses a minimal v2 network status document. + """ + + document = get_network_status_document_v2() + + self.assertEquals((), document.routers) + self.assertEquals(2, document.version) + self.assertEquals("18.244.0.114", document.hostname) + self.assertEquals("18.244.0.114", document.address) + self.assertEquals(80, document.dir_port) + self.assertEquals("719BE45DE224B607C53707D0E2143E2D423E74CF", document.fingerprint) + self.assertEquals("arma at mit dot edu", document.contact) + self.assertEquals(NETWORK_STATUS_DOCUMENT_HEADER_V2[5][1][1:], document.signing_key) + self.assertEquals([], document.client_versions) + self.assertEquals([], document.server_versions) + self.assertEquals(datetime.datetime(2005, 12, 16, 0, 13, 46), document.published) + self.assertEquals([], document.options) + self.assertEquals("moria2", document.signing_authority) + self.assertEquals(NETWORK_STATUS_DOCUMENT_FOOTER_V2[0][1][7:], document.signature) +