commit 79612002788e1f9f80733974b17eefa976055d06 Author: Damian Johnson atagar@torproject.org Date: Mon Oct 15 19:12:15 2012 -0700
Controller methods for querying descriptor info
Adding a get_server_descriptor() and get_network_status() method for querying server descriptors and router status entries. --- stem/control.py | 54 ++++++++++++++++++++++++ stem/descriptor/server_descriptor.py | 12 +++++ stem/util/tor_tools.py | 7 +++- test/integ/control/controller.py | 77 ++++++++++++++++++++++++++++++++++ 4 files changed, 149 insertions(+), 1 deletions(-)
diff --git a/stem/control.py b/stem/control.py index a5a7868..6077564 100644 --- a/stem/control.py +++ b/stem/control.py @@ -32,6 +32,8 @@ interacting at a higher level. |- repurpose_circuit - change a circuit's purpose |- map_address - maps one address to another such that connections to the original are replaced with the other |- get_version - convenience method to get tor version + |- get_server_descriptor - querying the server descriptor for a relay + |- get_network_status - querying the router status entry for a relay |- authenticate - convenience method to authenticate the controller +- protocolinfo - convenience method to get the protocol info
@@ -55,6 +57,8 @@ import threading import stem.response import stem.socket import stem.version +import stem.descriptor.router_status_entry +import stem.descriptor.server_descriptor import stem.util.connection import stem.util.log as log
@@ -650,6 +654,56 @@ class Controller(BaseController):
return self._request_cache["version"]
+ def get_server_descriptor(self, relay): + """ + Provides the server descriptor for the relay with the given fingerprint or + nickname. If the relay identifier could be either a fingerprint *or* + nickname then it's queried as a fingerprint. + + :param str relay: fingerprint or nickname of the relay to be queried + + :returns: :class:`stem.descriptor.server_descriptor.RelayDescriptor` for the given relay + + :raises: + * :class:`stem.socket.ControllerError` if unable to query the descriptor + * ValueError if **relay** doesn't conform with the patter for being a fingerprint or nickname + """ + + if stem.util.tor_tools.is_valid_fingerprint(relay): + query = "desc/id/%s" % relay + elif stem.util.tor_tools.is_valid_nickname(relay): + query = "desc/name/%s" % relay + else: + raise ValueError("'%s' isn't a valid fingerprint or nickname" % relay) + + desc_content = self.get_info(query) + return stem.descriptor.server_descriptor.RelayDescriptor(desc_content) + + def get_network_status(self, relay): + """ + Provides the router status entry for the relay with the given fingerprint + or nickname. If the relay identifier could be either a fingerprint *or* + nickname then it's queried as a fingerprint. + + :param str relay: fingerprint or nickname of the relay to be queried + + :returns: :class:`stem.descriptor.router_status_entry.RouterStatusEntryV2` for the given relay + + :raises: + * :class:`stem.socket.ControllerError` if unable to query the descriptor + * ValueError if **relay** doesn't conform with the patter for being a fingerprint or nickname + """ + + if stem.util.tor_tools.is_valid_fingerprint(relay): + query = "ns/id/%s" % relay + elif stem.util.tor_tools.is_valid_nickname(relay): + query = "ns/name/%s" % relay + else: + raise ValueError("'%s' isn't a valid fingerprint or nickname" % relay) + + desc_content = self.get_info(query) + return stem.descriptor.router_status_entry.RouterStatusEntryV2(desc_content) + def authenticate(self, *args, **kwargs): """ A convenience method to authenticate the controller. diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py index 5e5e04a..b597de9 100644 --- a/stem/descriptor/server_descriptor.py +++ b/stem/descriptor/server_descriptor.py @@ -631,6 +631,12 @@ class RelayDescriptor(ServerDescriptor): del entries["router-signature"]
ServerDescriptor._parse(self, entries, validate) + + def __cmp__(self, other): + if not isinstance(other, RelayDescriptor): + return 1 + + return str(self).strip() > str(other).strip()
class BridgeDescriptor(ServerDescriptor): """ @@ -762,4 +768,10 @@ class BridgeDescriptor(ServerDescriptor):
def _last_keyword(self): return None + + def __cmp__(self, other): + if not isinstance(other, BridgeDescriptor): + return 1 + + return str(self).strip() > str(other).strip()
diff --git a/stem/util/tor_tools.py b/stem/util/tor_tools.py index 7a980c7..a651445 100644 --- a/stem/util/tor_tools.py +++ b/stem/util/tor_tools.py @@ -29,7 +29,9 @@ def is_valid_fingerprint(entry, check_prefix = False): :returns: True if the string could be a relay fingerprint, False otherwise. """
- if check_prefix: + if not isinstance(entry, str): + return False + elif check_prefix: if not entry or entry[0] != "$": return False entry = entry[1:]
@@ -44,6 +46,9 @@ def is_valid_nickname(entry): :returns: True if the string could be a nickname, False otherwise. """
+ if not isinstance(entry, str): + return False + return bool(NICKNAME_PATTERN.match(entry))
def is_hex_digits(entry, count): diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index bd17703..1482c23 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -4,6 +4,7 @@ Integration tests for the stem.control.Controller class.
from __future__ import with_statement
+import os import re import shutil import socket @@ -16,6 +17,8 @@ import stem.version import stem.response.protocolinfo import test.runner import test.util +import stem.descriptor.router_status_entry +import stem.descriptor.reader
class TestController(unittest.TestCase): def test_from_port(self): @@ -424,4 +427,78 @@ class TestController(unittest.TestCase): ip_addr = response[response.find("\r\n\r\n"):].strip()
self.assertTrue(stem.util.connection.is_valid_ip_address(ip_addr)) + + def test_get_server_descriptor(self): + """ + Compares get_server_descriptor() against our cached descriptors. + """ + + runner = test.runner.get_runner() + descriptor_path = runner.get_test_dir("cached-descriptors") + + if test.runner.require_control(self): return + elif not os.path.exists(descriptor_path): + test.runner.skip(self, "(no cached descriptors)") + return + + with runner.get_tor_controller() as controller: + # we should balk at invalid content + self.assertRaises(ValueError, controller.get_server_descriptor, None) + self.assertRaises(ValueError, controller.get_server_descriptor, "") + self.assertRaises(ValueError, controller.get_server_descriptor, 5) + self.assertRaises(ValueError, controller.get_server_descriptor, "z" * 30) + + # try with a relay that doesn't exist + self.assertRaises(stem.socket.ControllerError, controller.get_server_descriptor, "blargg") + self.assertRaises(stem.socket.ControllerError, controller.get_server_descriptor, "5" * 40) + + first_descriptor = None + with stem.descriptor.reader.DescriptorReader([descriptor_path]) as reader: + for desc in reader: + if desc.nickname != "Unnamed": + first_descriptor = desc + break + + self.assertEqual(first_descriptor, controller.get_server_descriptor(first_descriptor.fingerprint)) + self.assertEqual(first_descriptor, controller.get_server_descriptor(first_descriptor.nickname)) + + def test_get_network_status(self): + """ + Compares get_network_status() against our cached descriptors. + """ + + runner = test.runner.get_runner() + descriptor_path = runner.get_test_dir("cached-consensus") + + if test.runner.require_control(self): return + elif not os.path.exists(descriptor_path): + test.runner.skip(self, "(no cached descriptors)") + return + + with runner.get_tor_controller() as controller: + # we should balk at invalid content + self.assertRaises(ValueError, controller.get_network_status, None) + self.assertRaises(ValueError, controller.get_network_status, "") + self.assertRaises(ValueError, controller.get_network_status, 5) + self.assertRaises(ValueError, controller.get_network_status, "z" * 30) + + # try with a relay that doesn't exist + self.assertRaises(stem.socket.ControllerError, controller.get_network_status, "blargg") + self.assertRaises(stem.socket.ControllerError, controller.get_network_status, "5" * 40) + + # our cached consensus is v3 but the control port can only be queried for + # v2 or v1 network status information + + first_descriptor = None + with stem.descriptor.reader.DescriptorReader([descriptor_path]) as reader: + for desc in reader: + if desc.nickname != "Unnamed": + # truncate to just the first couple lines and reconstruct as a v2 entry + truncated_content = "\n".join(str(desc).split("\n")[:2]) + + first_descriptor = stem.descriptor.router_status_entry.RouterStatusEntryV2(truncated_content) + break + + self.assertEqual(first_descriptor, controller.get_network_status(first_descriptor.fingerprint)) + self.assertEqual(first_descriptor, controller.get_network_status(first_descriptor.nickname))