[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Router status entry lazy loading
commit 1fcb9751dcbd7393fba61a6f78902160ad77304d
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date: Sun Jan 25 10:25:29 2015 -0800
Router status entry lazy loading
---
stem/descriptor/microdescriptor.py | 13 +-
stem/descriptor/router_status_entry.py | 688 ++++++++++++---------------
stem/descriptor/server_descriptor.py | 31 +-
test/unit/descriptor/router_status_entry.py | 13 +-
4 files changed, 320 insertions(+), 425 deletions(-)
diff --git a/stem/descriptor/microdescriptor.py b/stem/descriptor/microdescriptor.py
index fd9ef9b..3d9447f 100644
--- a/stem/descriptor/microdescriptor.py
+++ b/stem/descriptor/microdescriptor.py
@@ -66,7 +66,6 @@ Doing the same is trivial with server descriptors...
import hashlib
-import stem.descriptor.router_status_entry
import stem.exit_policy
from stem.descriptor import (
@@ -74,11 +73,15 @@ from stem.descriptor import (
_get_descriptor_components,
_read_until_keywords,
_value,
- _values,
_parse_simple_line,
_parse_key_block,
)
+from stem.descriptor.router_status_entry import (
+ _parse_a_line,
+ _parse_p_line,
+)
+
try:
# added in python 3.2
from functools import lru_cache
@@ -155,11 +158,6 @@ def _parse_file(descriptor_file, validate = True, **kwargs):
break # done parsing descriptors
-def _parse_a_line(descriptor, entries):
- for value in _values('a', entries):
- stem.descriptor.router_status_entry._parse_a_line(descriptor, value, True)
-
-
def _parse_id_line(descriptor, entries):
value = _value('id', entries)
value_comp = value.split()
@@ -174,7 +172,6 @@ def _parse_id_line(descriptor, entries):
_parse_onion_key_line = _parse_key_block('onion-key', 'onion_key', 'RSA PUBLIC KEY')
_parse_ntor_onion_key_line = _parse_simple_line('ntor-onion-key', 'ntor_onion_key')
_parse_family_line = lambda descriptor, entries: setattr(descriptor, 'family', _value('family', entries).split(' '))
-_parse_p_line = lambda descriptor, entries: stem.descriptor.router_status_entry._parse_p_line(descriptor, _value('p', entries), True)
_parse_p6_line = lambda descriptor, entries: setattr(descriptor, 'exit_policy_v6', stem.exit_policy.MicroExitPolicy(_value('p6', entries)))
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index ab93ada..f80c56b 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -29,6 +29,8 @@ import stem.util.str_tools
from stem.descriptor import (
KEYWORD_LINE,
Descriptor,
+ _value,
+ _values,
_get_descriptor_components,
_read_until_keywords,
)
@@ -101,6 +103,248 @@ def _parse_file(document_file, validate, entry_class, entry_keyword = 'r', start
break
+def _parse_r_line(descriptor, entries):
+ # Parses a RouterStatusEntry's 'r' line. They're very nearly identical for
+ # all current entry types (v2, v3, and microdescriptor v3) with one little
+ # wrinkle: only the microdescriptor flavor excludes a 'digest' field.
+ #
+ # For v2 and v3 router status entries:
+ # "r" nickname identity digest publication IP ORPort DirPort
+ # example: r mauer BD7xbfsCFku3+tgybEZsg8Yjhvw itcuKQ6PuPLJ7m/Oi928WjO2j8g 2012-06-22 13:19:32 80.101.105.103 9001 0
+ #
+ # For v3 microdescriptor router status entries:
+ # "r" nickname identity publication IP ORPort DirPort
+ # example: r Konata ARIJF2zbqirB9IwsW0mQznccWww 2012-09-24 13:40:40 69.64.48.168 9001 9030
+
+ value = _value('r', entries)
+ include_digest = not isinstance(descriptor, RouterStatusEntryMicroV3)
+
+ r_comp = value.split(' ')
+
+ # inject a None for the digest to normalize the field positioning
+ if not include_digest:
+ r_comp.insert(2, None)
+
+ if len(r_comp) < 8:
+ expected_field_count = 'eight' if include_digest else 'seven'
+ raise ValueError("%s 'r' line must have %s values: r %s" % (descriptor._name(), expected_field_count, value))
+
+ if not stem.util.tor_tools.is_valid_nickname(r_comp[0]):
+ raise ValueError("%s nickname isn't valid: %s" % (descriptor._name(), r_comp[0]))
+ elif not stem.util.connection.is_valid_ipv4_address(r_comp[5]):
+ raise ValueError("%s address isn't a valid IPv4 address: %s" % (descriptor._name(), r_comp[5]))
+ elif not stem.util.connection.is_valid_port(r_comp[6]):
+ raise ValueError('%s ORPort is invalid: %s' % (descriptor._name(), r_comp[6]))
+ elif not stem.util.connection.is_valid_port(r_comp[7], allow_zero = True):
+ raise ValueError('%s DirPort is invalid: %s' % (descriptor._name(), r_comp[7]))
+
+ descriptor.nickname = r_comp[0]
+ descriptor.fingerprint = _base64_to_hex(r_comp[1])
+
+ if include_digest:
+ descriptor.digest = _base64_to_hex(r_comp[2])
+
+ descriptor.address = r_comp[5]
+ descriptor.or_port = int(r_comp[6])
+ descriptor.dir_port = None if r_comp[7] == '0' else int(r_comp[7])
+
+ try:
+ published = '%s %s' % (r_comp[3], r_comp[4])
+ descriptor.published = stem.util.str_tools._parse_timestamp(published)
+ except ValueError:
+ raise ValueError("Publication time time wasn't parsable: r %s" % value)
+
+
+def _parse_a_line(descriptor, entries):
+ # "a" SP address ":" portlist
+ # example: a [2001:888:2133:0:82:94:251:204]:9001
+
+ or_addresses = []
+
+ for value in _values('a', entries):
+ if ':' not in value:
+ raise ValueError("%s 'a' line must be of the form '[address]:[ports]': a %s" % (descriptor._name(), value))
+
+ address, port = value.rsplit(':', 1)
+ is_ipv6 = address.startswith('[') and address.endswith(']')
+
+ if is_ipv6:
+ address = address[1:-1] # remove brackets
+
+ if not ((not is_ipv6 and stem.util.connection.is_valid_ipv4_address(address)) or
+ (is_ipv6 and stem.util.connection.is_valid_ipv6_address(address))):
+ raise ValueError("%s 'a' line must start with an IPv6 address: a %s" % (descriptor._name(), value))
+
+ if stem.util.connection.is_valid_port(port):
+ or_addresses.append((address, int(port), is_ipv6))
+ else:
+ raise ValueError("%s 'a' line had an invalid port (%s): a %s" % (descriptor._name(), port, value))
+
+ descriptor.or_addresses = or_addresses
+
+
+def _parse_s_line(descriptor, entries):
+ # "s" Flags
+ # example: s Named Running Stable Valid
+
+ value = _value('s', entries)
+ flags = [] if value == '' else value.split(' ')
+ descriptor.flags = flags
+
+ for flag in flags:
+ if flags.count(flag) > 1:
+ raise ValueError('%s had duplicate flags: s %s' % (descriptor._name(), value))
+ elif flag == "":
+ raise ValueError("%s had extra whitespace on its 's' line: s %s" % (descriptor._name(), value))
+
+
+def _parse_v_line(descriptor, entries):
+ # "v" version
+ # example: 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".
+
+ value = _value('v', entries)
+ descriptor.version_line = value
+
+ if value.startswith('Tor '):
+ try:
+ descriptor.version = stem.version._get_version(value[4:])
+ except ValueError as exc:
+ raise ValueError('%s has a malformed tor version (%s): v %s' % (descriptor._name(), exc, value))
+
+
+def _parse_w_line(descriptor, entries):
+ # "w" "Bandwidth=" INT ["Measured=" INT] ["Unmeasured=1"]
+ # example: w Bandwidth=7980
+
+ value = _value('w', entries)
+ w_comp = value.split(' ')
+
+ if len(w_comp) < 1:
+ raise ValueError("%s 'w' line is blank: w %s" % (descriptor._name(), value))
+ elif not w_comp[0].startswith('Bandwidth='):
+ raise ValueError("%s 'w' line needs to start with a 'Bandwidth=' entry: w %s" % (descriptor._name(), value))
+
+ for w_entry in w_comp:
+ 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 and w_value.isdigit()):
+ raise ValueError("%s 'Bandwidth=' entry needs to have a numeric value: w %s" % (descriptor._name(), value))
+
+ descriptor.bandwidth = int(w_value)
+ elif w_key == 'Measured':
+ if not (w_value and w_value.isdigit()):
+ raise ValueError("%s 'Measured=' entry needs to have a numeric value: w %s" % (descriptor._name(), value))
+
+ descriptor.measured = int(w_value)
+ elif w_key == 'Unmeasured':
+ if w_value != '1':
+ raise ValueError("%s 'Unmeasured=' should only have the value of '1': w %s" % (descriptor._name(), value))
+
+ descriptor.is_unmeasured = True
+ else:
+ descriptor.unrecognized_bandwidth_entries.append(w_entry)
+
+
+def _parse_p_line(descriptor, entries):
+ # "p" ("accept" / "reject") PortList
+ # p reject 1-65535
+ # example: p accept 80,110,143,443,993,995,6660-6669,6697,7000-7001
+
+ value = _value('p', entries)
+
+ try:
+ descriptor.exit_policy = stem.exit_policy.MicroExitPolicy(value)
+ except ValueError as exc:
+ raise ValueError('%s exit policy is malformed (%s): p %s' % (descriptor._name(), exc, value))
+
+
+def _parse_m_line(descriptor, entries):
+ # "m" methods 1*(algorithm "=" digest)
+ # example: m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs
+
+ all_hashes = []
+
+ for value in _values('m', entries):
+ m_comp = value.split(' ')
+
+ if not (descriptor.document and descriptor.document.is_vote):
+ vote_status = 'vote' if descriptor.document else '<undefined document>'
+ raise ValueError("%s 'm' line should only appear in votes (appeared in a %s): m %s" % (descriptor._name(), vote_status, value))
+ elif len(m_comp) < 1:
+ raise ValueError("%s 'm' line needs to start with a series of methods: m %s" % (descriptor._name(), value))
+
+ try:
+ methods = [int(entry) for entry in m_comp[0].split(',')]
+ except ValueError:
+ raise ValueError('%s microdescriptor methods should be a series of comma separated integers: m %s' % (descriptor._name(), value))
+
+ hashes = {}
+
+ for entry in m_comp[1:]:
+ if '=' not in entry:
+ raise ValueError("%s can only have a series of 'algorithm=digest' mappings after the methods: m %s" % (descriptor._name(), value))
+
+ hash_name, digest = entry.split('=', 1)
+ hashes[hash_name] = digest
+
+ all_hashes.append((methods, hashes))
+
+ descriptor.microdescriptor_hashes = all_hashes
+
+
+def _parse_microdescriptor_m_line(descriptor, entries):
+ # "m" digest
+ # example: m aiUklwBrua82obG5AsTX+iEpkjQA2+AQHxZ7GwMfY70
+
+ descriptor.digest = _base64_to_hex(_value('m', entries), check_if_fingerprint = False)
+
+
+def _base64_to_hex(identity, check_if_fingerprint = True):
+ """
+ Decodes a base64 value to hex. For example...
+
+ ::
+
+ >>> _base64_to_hex('p1aag7VwarGxqctS7/fS0y5FU+s')
+ 'A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB'
+
+ :param str identity: encoded fingerprint from the consensus
+ :param bool check_if_fingerprint: asserts that the result is a fingerprint if **True**
+
+ :returns: **str** with the uppercase hex encoding of the relay's fingerprint
+
+ :raises: **ValueError** if the result isn't a valid fingerprint
+ """
+
+ # trailing equal signs were stripped from the identity
+ missing_padding = len(identity) % 4
+ identity += '=' * missing_padding
+
+ try:
+ identity_decoded = base64.b64decode(stem.util.str_tools._to_bytes(identity))
+ except (TypeError, binascii.Error):
+ raise ValueError("Unable to decode identity string '%s'" % identity)
+
+ fingerprint = binascii.b2a_hex(identity_decoded).upper()
+
+ if stem.prereq.is_python_3():
+ fingerprint = stem.util.str_tools._to_unicode(fingerprint)
+
+ if check_if_fingerprint:
+ 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))
+
+ return fingerprint
+
+
class RouterStatusEntry(Descriptor):
"""
Information about an individual router stored within a network status
@@ -122,7 +366,27 @@ class RouterStatusEntry(Descriptor):
:var str version_line: versioning information reported by the relay
"""
- def __init__(self, content, validate, document):
+ ATTRIBUTES = {
+ 'nickname': (None, _parse_r_line),
+ 'fingerprint': (None, _parse_r_line),
+ 'published': (None, _parse_r_line),
+ 'address': (None, _parse_r_line),
+ 'or_port': (None, _parse_r_line),
+ 'dir_port': (None, _parse_r_line),
+
+ 'flags': (None, _parse_s_line),
+
+ 'version_line': (None, _parse_v_line),
+ 'version': (None, _parse_v_line),
+ }
+
+ PARSER_FOR_LINE = {
+ 'r': _parse_r_line,
+ 's': _parse_s_line,
+ 'v': _parse_v_line,
+ }
+
+ def __init__(self, content, validate = True, document = None):
"""
Parse a router descriptor in a network status document.
@@ -134,51 +398,17 @@ class RouterStatusEntry(Descriptor):
:raises: **ValueError** if the descriptor data is invalid
"""
- super(RouterStatusEntry, self).__init__(content)
+ super(RouterStatusEntry, self).__init__(content, lazy_load = not validate)
content = stem.util.str_tools._to_unicode(content)
self.document = document
-
- self.nickname = None
- self.fingerprint = None
- self.published = None
- self.address = None
- self.or_port = None
- self.dir_port = None
-
- self.flags = None
-
- self.version_line = None
- self.version = None
-
- self._unrecognized_lines = []
-
entries = _get_descriptor_components(content, validate)
if validate:
self._check_constraints(entries)
-
- self._parse(entries, validate)
-
- def _parse(self, entries, validate):
- """
- Parses the given content and applies the attributes.
-
- :param dict entries: keyword => (value, pgp key) entries
- :param bool validate: checks validity if **True**
-
- :raises: **ValueError** if a validity check fails
- """
-
- for keyword, values in list(entries.items()):
- value, _, _ = values[0]
-
- if keyword == 's':
- _parse_s_line(self, value, validate)
- elif keyword == 'v':
- _parse_v_line(self, value, validate)
- else:
- self._unrecognized_lines.append('%s %s' % (keyword, value))
+ self._parse(entries, validate)
+ else:
+ self._entries = entries
def _check_constraints(self, entries):
"""
@@ -225,15 +455,6 @@ class RouterStatusEntry(Descriptor):
return ()
- def get_unrecognized_lines(self):
- """
- Provides any unrecognized lines.
-
- :returns: list of unrecognized lines
- """
-
- return list(self._unrecognized_lines)
-
def _compare(self, other, method):
if not isinstance(other, RouterStatusEntry):
return False
@@ -261,19 +482,9 @@ class RouterStatusEntryV2(RouterStatusEntry):
a default value, others are left as **None** if undefined
"""
- def __init__(self, content, validate = True, document = None):
- self.digest = None
- super(RouterStatusEntryV2, self).__init__(content, validate, document)
-
- def _parse(self, entries, validate):
- for keyword, values in list(entries.items()):
- value, _, _ = values[0]
-
- if keyword == 'r':
- _parse_r_line(self, value, validate, True)
- del entries['r']
-
- RouterStatusEntry._parse(self, entries, validate)
+ ATTRIBUTES = dict(RouterStatusEntry.ATTRIBUTES, **{
+ 'digest': (None, _parse_r_line),
+ })
def _name(self, is_plural = False):
if is_plural:
@@ -331,45 +542,25 @@ class RouterStatusEntryV3(RouterStatusEntry):
a default value, others are left as **None** if undefined
"""
- def __init__(self, content, validate = True, document = None):
- self.or_addresses = []
- self.digest = None
-
- self.bandwidth = None
- self.measured = None
- self.is_unmeasured = False
- self.unrecognized_bandwidth_entries = []
-
- self.exit_policy = None
- self.microdescriptor_hashes = []
-
- super(RouterStatusEntryV3, self).__init__(content, validate, document)
+ ATTRIBUTES = dict(RouterStatusEntry.ATTRIBUTES, **{
+ 'digest': (None, _parse_r_line),
+ 'or_addresses': ([], _parse_a_line),
- def _parse(self, entries, validate):
- for keyword, values in list(entries.items()):
- value, _, _ = values[0]
+ 'bandwidth': (None, _parse_w_line),
+ 'measured': (None, _parse_w_line),
+ 'is_unmeasured': (False, _parse_w_line),
+ 'unrecognized_bandwidth_entries': ([], _parse_w_line),
- if keyword == 'r':
- _parse_r_line(self, value, validate, True)
- del entries['r']
- elif keyword == 'a':
- for entry, _, _ in values:
- _parse_a_line(self, entry, validate)
+ 'exit_policy': (None, _parse_p_line),
+ 'microdescriptor_hashes': ([], _parse_m_line),
+ })
- del entries['a']
- elif keyword == 'w':
- _parse_w_line(self, value, validate)
- del entries['w']
- elif keyword == 'p':
- _parse_p_line(self, value, validate)
- del entries['p']
- elif keyword == 'm':
- for entry, _, _ in values:
- _parse_m_line(self, entry, validate)
-
- del entries['m']
-
- RouterStatusEntry._parse(self, entries, validate)
+ PARSER_FOR_LINE = dict(RouterStatusEntry.PARSER_FOR_LINE, **{
+ 'a': _parse_a_line,
+ 'w': _parse_w_line,
+ 'p': _parse_p_line,
+ 'm': _parse_m_line,
+ })
def _name(self, is_plural = False):
if is_plural:
@@ -417,34 +608,19 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
a default value, others are left as **None** if undefined
"""
- def __init__(self, content, validate = True, document = None):
- self.bandwidth = None
- self.measured = None
- self.is_unmeasured = False
- self.unrecognized_bandwidth_entries = []
-
- self.digest = None
-
- super(RouterStatusEntryMicroV3, self).__init__(content, validate, document)
-
- def _parse(self, entries, validate):
- for keyword, values in list(entries.items()):
- value, _, _ = values[0]
+ ATTRIBUTES = dict(RouterStatusEntry.ATTRIBUTES, **{
+ 'bandwidth': (None, _parse_w_line),
+ 'measured': (None, _parse_w_line),
+ 'is_unmeasured': (False, _parse_w_line),
+ 'unrecognized_bandwidth_entries': ([], _parse_w_line),
- if keyword == 'r':
- _parse_r_line(self, value, validate, False)
- del entries['r']
- elif keyword == 'w':
- _parse_w_line(self, value, validate)
- del entries['w']
- elif keyword == 'm':
- # "m" digest
- # example: m aiUklwBrua82obG5AsTX+iEpkjQA2+AQHxZ7GwMfY70
+ 'digest': (None, _parse_microdescriptor_m_line),
+ })
- self.digest = _base64_to_hex(value, validate, False)
- del entries['m']
-
- RouterStatusEntry._parse(self, entries, validate)
+ PARSER_FOR_LINE = dict(RouterStatusEntry.PARSER_FOR_LINE, **{
+ 'w': _parse_w_line,
+ 'm': _parse_microdescriptor_m_line,
+ })
def _name(self, is_plural = False):
if is_plural:
@@ -472,269 +648,3 @@ class RouterStatusEntryMicroV3(RouterStatusEntry):
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
-
-
-def _parse_r_line(desc, value, validate, include_digest = True):
- # Parses a RouterStatusEntry's 'r' line. They're very nearly identical for
- # all current entry types (v2, v3, and microdescriptor v3) with one little
- # wrinkle: only the microdescriptor flavor excludes a 'digest' field.
- #
- # For v2 and v3 router status entries:
- # "r" nickname identity digest publication IP ORPort DirPort
- # example: r mauer BD7xbfsCFku3+tgybEZsg8Yjhvw itcuKQ6PuPLJ7m/Oi928WjO2j8g 2012-06-22 13:19:32 80.101.105.103 9001 0
- #
- # For v3 microdescriptor router status entries:
- # "r" nickname identity publication IP ORPort DirPort
- # example: r Konata ARIJF2zbqirB9IwsW0mQznccWww 2012-09-24 13:40:40 69.64.48.168 9001 9030
-
- r_comp = value.split(' ')
-
- # inject a None for the digest to normalize the field positioning
- if not include_digest:
- r_comp.insert(2, None)
-
- if len(r_comp) < 8:
- if not validate:
- return
-
- expected_field_count = 'eight' if include_digest else 'seven'
- raise ValueError("%s 'r' line must have %s values: r %s" % (desc._name(), expected_field_count, value))
-
- if validate:
- if not stem.util.tor_tools.is_valid_nickname(r_comp[0]):
- raise ValueError("%s nickname isn't valid: %s" % (desc._name(), r_comp[0]))
- elif not stem.util.connection.is_valid_ipv4_address(r_comp[5]):
- raise ValueError("%s address isn't a valid IPv4 address: %s" % (desc._name(), r_comp[5]))
- elif not stem.util.connection.is_valid_port(r_comp[6]):
- raise ValueError('%s ORPort is invalid: %s' % (desc._name(), r_comp[6]))
- elif not stem.util.connection.is_valid_port(r_comp[7], allow_zero = True):
- raise ValueError('%s DirPort is invalid: %s' % (desc._name(), r_comp[7]))
- elif not (r_comp[6].isdigit() and r_comp[7].isdigit()):
- return
-
- desc.nickname = r_comp[0]
- desc.fingerprint = _base64_to_hex(r_comp[1], validate)
-
- if include_digest:
- desc.digest = _base64_to_hex(r_comp[2], validate)
-
- desc.address = r_comp[5]
- desc.or_port = int(r_comp[6])
- desc.dir_port = None if r_comp[7] == '0' else int(r_comp[7])
-
- try:
- published = '%s %s' % (r_comp[3], r_comp[4])
- desc.published = stem.util.str_tools._parse_timestamp(published)
- except ValueError:
- if validate:
- raise ValueError("Publication time time wasn't parsable: r %s" % value)
-
-
-def _parse_a_line(desc, value, validate):
- # "a" SP address ":" portlist
- # example: a [2001:888:2133:0:82:94:251:204]:9001
-
- if ':' not in value:
- if not validate:
- return
-
- raise ValueError("%s 'a' line must be of the form '[address]:[ports]': a %s" % (desc._name(), value))
-
- address, port = value.rsplit(':', 1)
- is_ipv6 = address.startswith('[') and address.endswith(']')
-
- if is_ipv6:
- address = address[1:-1] # remove brackets
-
- if not ((not is_ipv6 and stem.util.connection.is_valid_ipv4_address(address)) or
- (is_ipv6 and stem.util.connection.is_valid_ipv6_address(address))):
- if not validate:
- return
- else:
- raise ValueError("%s 'a' line must start with an IPv6 address: a %s" % (desc._name(), value))
-
- if stem.util.connection.is_valid_port(port):
- desc.or_addresses.append((address, int(port), is_ipv6))
- elif validate:
- raise ValueError("%s 'a' line had an invalid port (%s): a %s" % (desc._name(), port, value))
-
-
-def _parse_s_line(desc, value, validate):
- # "s" Flags
- # example: s Named Running Stable Valid
-
- flags = [] if value == '' else value.split(' ')
- desc.flags = flags
-
- if validate:
- for flag in flags:
- if flags.count(flag) > 1:
- raise ValueError('%s had duplicate flags: s %s' % (desc._name(), value))
- elif flag == "":
- raise ValueError("%s had extra whitespace on its 's' line: s %s" % (desc._name(), value))
-
-
-def _parse_v_line(desc, value, validate):
- # "v" version
- # example: 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".
-
- desc.version_line = value
-
- if value.startswith('Tor '):
- try:
- desc.version = stem.version._get_version(value[4:])
- except ValueError as exc:
- if validate:
- raise ValueError('%s has a malformed tor version (%s): v %s' % (desc._name(), exc, value))
-
-
-def _parse_w_line(desc, value, validate):
- # "w" "Bandwidth=" INT ["Measured=" INT] ["Unmeasured=1"]
- # example: w Bandwidth=7980
-
- w_comp = value.split(' ')
-
- if len(w_comp) < 1:
- if not validate:
- return
-
- raise ValueError("%s 'w' line is blank: w %s" % (desc._name(), value))
- elif not w_comp[0].startswith('Bandwidth='):
- if not validate:
- return
-
- raise ValueError("%s 'w' line needs to start with a 'Bandwidth=' entry: w %s" % (desc._name(), value))
-
- for w_entry in w_comp:
- 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 and w_value.isdigit()):
- if not validate:
- return
-
- raise ValueError("%s 'Bandwidth=' entry needs to have a numeric value: w %s" % (desc._name(), value))
-
- desc.bandwidth = int(w_value)
- elif w_key == 'Measured':
- if not (w_value and w_value.isdigit()):
- if not validate:
- return
-
- raise ValueError("%s 'Measured=' entry needs to have a numeric value: w %s" % (desc._name(), value))
-
- desc.measured = int(w_value)
- elif w_key == 'Unmeasured':
- if validate and w_value != '1':
- raise ValueError("%s 'Unmeasured=' should only have the value of '1': w %s" % (desc._name(), value))
-
- desc.is_unmeasured = True
- else:
- desc.unrecognized_bandwidth_entries.append(w_entry)
-
-
-def _parse_p_line(desc, value, validate):
- # "p" ("accept" / "reject") PortList
- # p reject 1-65535
- # example: p accept 80,110,143,443,993,995,6660-6669,6697,7000-7001
-
- try:
- desc.exit_policy = stem.exit_policy.MicroExitPolicy(value)
- except ValueError as exc:
- if not validate:
- return
-
- raise ValueError('%s exit policy is malformed (%s): p %s' % (desc._name(), exc, value))
-
-
-def _parse_m_line(desc, value, validate):
- # "m" methods 1*(algorithm "=" digest)
- # example: m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs
-
- m_comp = value.split(' ')
-
- if not (desc.document and desc.document.is_vote):
- if not validate:
- return
-
- vote_status = 'vote' if desc.document else '<undefined document>'
- raise ValueError("%s 'm' line should only appear in votes (appeared in a %s): m %s" % (desc._name(), vote_status, value))
- elif len(m_comp) < 1:
- if not validate:
- return
-
- raise ValueError("%s 'm' line needs to start with a series of methods: m %s" % (desc._name(), value))
-
- try:
- methods = [int(entry) for entry in m_comp[0].split(',')]
- except ValueError:
- if not validate:
- return
-
- raise ValueError('%s microdescriptor methods should be a series of comma separated integers: m %s' % (desc._name(), value))
-
- hashes = {}
-
- for entry in m_comp[1:]:
- if '=' not in entry:
- if not validate:
- continue
-
- raise ValueError("%s can only have a series of 'algorithm=digest' mappings after the methods: m %s" % (desc._name(), value))
-
- hash_name, digest = entry.split('=', 1)
- hashes[hash_name] = digest
-
- desc.microdescriptor_hashes.append((methods, hashes))
-
-
-def _base64_to_hex(identity, validate, check_if_fingerprint = True):
- """
- Decodes a base64 value to hex. For example...
-
- ::
-
- >>> _base64_to_hex('p1aag7VwarGxqctS7/fS0y5FU+s', True)
- 'A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB'
-
- :param str identity: encoded fingerprint from the consensus
- :param bool validate: checks validity if **True**
- :param bool check_if_fingerprint: asserts that the result is a fingerprint if **True**
-
- :returns: **str** with the uppercase hex encoding of the relay's fingerprint
-
- :raises: **ValueError** if the result isn't a valid fingerprint
- """
-
- # trailing equal signs were stripped from the identity
- missing_padding = len(identity) % 4
- identity += '=' * missing_padding
-
- try:
- identity_decoded = base64.b64decode(stem.util.str_tools._to_bytes(identity))
- except (TypeError, binascii.Error):
- if not validate:
- return None
-
- raise ValueError("Unable to decode identity string '%s'" % identity)
-
- fingerprint = binascii.b2a_hex(identity_decoded).upper()
-
- if stem.prereq.is_python_3():
- fingerprint = stem.util.str_tools._to_unicode(fingerprint)
-
- if check_if_fingerprint:
- if not stem.util.tor_tools.is_valid_fingerprint(fingerprint):
- 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/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index 1cdf9be..c4f4bca 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -537,6 +537,16 @@ class ServerDescriptor(Descriptor):
if validate:
self._parse(entries, validate)
+
+ _parse_exit_policy(self, entries)
+
+ # if we have a negative uptime and a tor version that shouldn't exhibit
+ # this bug then fail validation
+
+ if validate and self.uptime and self.tor_version:
+ if self.uptime < 0 and self.tor_version >= stem.version.Version('0.1.2.7'):
+ raise ValueError("Descriptor for version '%s' had a negative uptime value: %i" % (self.tor_version, self.uptime))
+
self._check_constraints(entries)
else:
self._entries = entries
@@ -588,27 +598,6 @@ class ServerDescriptor(Descriptor):
return self._annotation_lines
- def _parse(self, entries, validate):
- """
- Parses a series of 'keyword => (value, pgp block)' mappings and applies
- them as attributes.
-
- :param dict entries: descriptor contents to be applied
- :param bool validate: checks the validity of descriptor content if **True**
-
- :raises: **ValueError** if an error occurs in validation
- """
-
- super(ServerDescriptor, self)._parse(entries, validate)
- _parse_exit_policy(self, entries)
-
- # if we have a negative uptime and a tor version that shouldn't exhibit
- # this bug then fail validation
-
- if validate and self.uptime and self.tor_version:
- if self.uptime < 0 and self.tor_version >= stem.version.Version('0.1.2.7'):
- raise ValueError("Descriptor for version '%s' had a negative uptime value: %i" % (self.tor_version, self.uptime))
-
def _check_constraints(self, entries):
"""
Does a basic check that the entries conform to this descriptor type's
diff --git a/test/unit/descriptor/router_status_entry.py b/test/unit/descriptor/router_status_entry.py
index 50924de..1243a55 100644
--- a/test/unit/descriptor/router_status_entry.py
+++ b/test/unit/descriptor/router_status_entry.py
@@ -39,8 +39,7 @@ class TestRouterStatusEntry(unittest.TestCase):
# checks with some malformed inputs
for arg in ('', '20wYcb', '20wYcb' * 30):
- self.assertRaises(ValueError, _base64_to_hex, arg, True)
- self.assertEqual(None, _base64_to_hex(arg, False))
+ self.assertRaises(ValueError, _base64_to_hex, arg)
def test_minimal_v2(self):
"""
@@ -138,7 +137,8 @@ class TestRouterStatusEntry(unittest.TestCase):
"""
content = b'z some stuff\n' + get_router_status_entry_v3(content = True)
- self._expect_invalid_attr(content, '_unrecognized_lines', ['z some stuff'])
+ self.assertRaises(ValueError, RouterStatusEntryV3, content)
+ self.assertEqual(['z some stuff'], RouterStatusEntryV3(content, False).get_unrecognized_lines())
def test_blank_lines(self):
"""
@@ -215,7 +215,7 @@ class TestRouterStatusEntry(unittest.TestCase):
if value == '':
value = None
- self._expect_invalid_attr(content, 'nickname', value)
+ self._expect_invalid_attr(content, 'nickname')
def test_malformed_fingerprint(self):
"""
@@ -275,7 +275,7 @@ class TestRouterStatusEntry(unittest.TestCase):
for value in test_values:
r_line = ROUTER_STATUS_ENTRY_V3_HEADER[0][1].replace('71.35.150.29', value)
content = get_router_status_entry_v3({'r': r_line}, content = True)
- self._expect_invalid_attr(content, 'address', value)
+ self._expect_invalid_attr(content, 'address')
def test_malformed_port(self):
"""
@@ -304,10 +304,9 @@ class TestRouterStatusEntry(unittest.TestCase):
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_v3({'r': r_line}, content = True)
- self._expect_invalid_attr(content, attr, expected)
+ self._expect_invalid_attr(content, attr)
def test_ipv6_addresses(self):
"""
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits