commit 450390e0ce57680d0ae37853b26bec4e90e26d6b Author: Damian Johnson atagar@torproject.org Date: Wed Jun 27 15:57:37 2018 -0700
Support vote's new bandwidth-file fields
Support for a newly added network status vote field...
https://gitweb.torproject.org/torspec.git/commit/?id=84591df
While whipping this up it crossed my mind that the spec is uncommonly loose for this one. Some examples of odd valid content are...
* Empty lines: 'bandwidth-file' * Multiple equals: 'bandwidth-file timestamp=12=34' * Inclusion of these lines in the consensus.
Pointed these out to juga. Also, imho 'headers' should be in the name somewhere since that's what these are (not a path or file content itself). --- docs/change_log.rst | 1 + stem/descriptor/networkstatus.py | 30 ++++++++++++++++++++++- test/unit/descriptor/networkstatus/document_v3.py | 26 ++++++++++++++++++++ 3 files changed, 56 insertions(+), 1 deletion(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index e735d6b2..eeaf0362 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -66,6 +66,7 @@ The following are only available within Stem's `git repository * `Fallback directory v2 support https://lists.torproject.org/pipermail/tor-dev/2017-December/012721.html`_, which adds *nickname* and *extrainfo* * Added the *orport_v6* attribute to the :class:`~stem.directory.Authority` class * Added server descriptor's new is_hidden_service_dir attribute + * Added the network status vote's new bandwidth_file attribute (:spec:`84591df`) * Don't retry downloading descriptors when we've timed out * Don't download from tor26 and Bifroest, which are authorities that frequently timeout * `stem.descriptor.remote <api/descriptor/remote.html>`_ now consistently defaults **fall_back_to_authority** to false diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py index 1be802f5..0e1af80b 100644 --- a/stem/descriptor/networkstatus.py +++ b/stem/descriptor/networkstatus.py @@ -130,6 +130,7 @@ HEADER_STATUS_DOCUMENT_FIELDS = ( ('shared-rand-commit', True, False, False), ('shared-rand-previous-value', True, True, False), ('shared-rand-current-value', True, True, False), + ('bandwidth-file', True, False, False), ('recommended-client-protocols', True, True, False), ('recommended-relay-protocols', True, True, False), ('required-client-protocols', True, True, False), @@ -783,6 +784,27 @@ def _parse_shared_rand_current_value(descriptor, entries): raise ValueError("A network status document's 'shared-rand-current-value' line must be a pair of values, the first an integer but was '%s'" % value)
+def _parse_bandwidth_file(descriptor, entries): + # "bandwidth-file" KeyValues + # Value ::= any printing ASCII character except NL and SP. + # KeyValue ::= Keyword '=' Value + # KeyValues ::= KeyValue | KeyValues SP KeyValue + + value = _value('bandwidth-file', entries) + results = {} + + for entry in value.split(' '): + if not entry: + continue + elif '=' not in entry: + raise ValueError("'bandwidth-file' lines must be a series of 'key=value' pairs: %s" % value) + + k, v = entry.split('=', 1) + results[k] = v + + descriptor.bandwidth_file = results + + _parse_header_valid_after_line = _parse_timestamp_line('valid-after', 'valid_after') _parse_header_fresh_until_line = _parse_timestamp_line('fresh-until', 'fresh_until') _parse_header_valid_until_line = _parse_timestamp_line('valid-until', 'valid_until') @@ -852,6 +874,7 @@ class NetworkStatusDocumentV3(NetworkStatusDocument): :var dict recommended_relay_protocols: recommended protocols for relays :var dict required_client_protocols: required protocols for clients :var dict required_relay_protocols: required protocols for relays + :var dict bandwidth_file: bandwidth authority headers that generated this vote
***** attribute is either required when we're parsed with validation or has a default value, others are left as None if undefined @@ -868,7 +891,7 @@ class NetworkStatusDocumentV3(NetworkStatusDocument):
.. versionchanged:: 1.6.0 Added the recommended_client_protocols, recommended_relay_protocols, - required_client_protocols, and required_relay_protocols. + required_client_protocols, and required_relay_protocols attributes.
.. versionchanged:: 1.6.0 The is_shared_randomness_participate and shared_randomness_commitments @@ -880,6 +903,9 @@ class NetworkStatusDocumentV3(NetworkStatusDocument): shared_randomness_previous_reveal_count attributes were undocumented and not provided properly if retrieved before their shred_randomness_*_value counterpart. + + .. versionchanged:: 1.7.0 + Added the bandwidth_file attributbute. """
ATTRIBUTES = { @@ -910,6 +936,7 @@ class NetworkStatusDocumentV3(NetworkStatusDocument): 'shared_randomness_previous_value': (None, _parse_shared_rand_previous_value), 'shared_randomness_current_reveal_count': (None, _parse_shared_rand_current_value), 'shared_randomness_current_value': (None, _parse_shared_rand_current_value), + 'bandwidth_file': ({}, _parse_bandwidth_file),
'signatures': ([], _parse_footer_directory_signature_line), 'bandwidth_weights': ({}, _parse_footer_bandwidth_weights_line), @@ -937,6 +964,7 @@ class NetworkStatusDocumentV3(NetworkStatusDocument): 'params': _parse_header_parameters_line, 'shared-rand-previous-value': _parse_shared_rand_previous_value, 'shared-rand-current-value': _parse_shared_rand_current_value, + 'bandwidth-file': _parse_bandwidth_file, }
FOOTER_PARSER_FOR_LINE = { diff --git a/test/unit/descriptor/networkstatus/document_v3.py b/test/unit/descriptor/networkstatus/document_v3.py index 20b38563..6b11fe6c 100644 --- a/test/unit/descriptor/networkstatus/document_v3.py +++ b/test/unit/descriptor/networkstatus/document_v3.py @@ -1255,6 +1255,32 @@ DnN5aFtYKiTc19qIC7Nmo+afPdDEf0MlJvEOP5EWl3w= self.assertEqual(None, authority.shared_randomness_current_reveal_count) self.assertEqual(None, authority.shared_randomness_current_value)
+ def test_bandwidth_file(self): + """ + Parses a 'bandwidth-file' line of votes. + """ + + test_values = { + '': {}, + 'timestamp=': {'timestamp': ''}, + 'timestamp=12=34': {'timestamp': '12=34'}, + 'timestamp=123': {'timestamp': '123'}, + 'timestamp=123 version=1.0': {'timestamp': '123', 'version': '1.0'}, + 'timestamp=123 version=1.0 software=neat_thingy': {'timestamp': '123', 'version': '1.0', 'software': 'neat_thingy'}, + } + + for test_value, expected_value in test_values.items(): + document = NetworkStatusDocumentV3.create({'vote-status': 'vote', 'bandwidth-file': test_value}) + self.assertEqual(expected_value, document.bandwidth_file) + + # there really isn't much that *isn't* valid :P + + content = NetworkStatusDocumentV3.content({'vote-status': 'vote', 'bandwidth-file': 'key_without_value'}) + self.assertRaises(ValueError, NetworkStatusDocumentV3, content, True) + + document = NetworkStatusDocumentV3(content, False) + self.assertEqual(DEFAULT_PARAMS, document.params) + def test_with_legacy_directory_authorities(self): """ Includes both normal authorities and those following the '-legacy' format.