[tor-commits] [stem/master] Controller methods for querying descriptor info

atagar at torproject.org atagar at torproject.org
Tue Oct 16 16:06:33 UTC 2012


commit 79612002788e1f9f80733974b17eefa976055d06
Author: Damian Johnson <atagar at 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))
 





More information about the tor-commits mailing list