[tor-commits] [stem/master] Support for fetching hidden service descriptors

atagar at torproject.org atagar at torproject.org
Sun Apr 26 03:01:45 UTC 2015


commit 8a90aa4d0b67efd40c911caf53b15f73b1b94b70
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Apr 25 20:03:06 2015 -0700

    Support for fetching hidden service descriptors
    
    Hazaa, HSFETCH was merged! Add a Controller method to take advantage of it so
    we can now provide hidden service descriptors.
---
 docs/change_log.rst              |    1 +
 stem/control.py                  |  100 ++++++++++++++++++++++++++++++++++++--
 stem/interpreter/settings.cfg    |    6 +++
 stem/response/events.py          |    2 +-
 stem/util/tor_tools.py           |   19 ++++++++
 stem/version.py                  |    4 ++
 test/integ/control/controller.py |   31 ++++++++++++
 test/unit/response/events.py     |    2 +-
 8 files changed, 159 insertions(+), 6 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index 47f2a49..4e2bf98 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -51,6 +51,7 @@ conversion (:trac:`14075`).
 
  * **Controller**
 
+  * Added :func:`~stem.control.Controller.get_hidden_service_descriptor` and `support for HS_DESC_CONTENT events <api/response.html#stem.response.events.HSDescContentEvent>`_ (:trac:`14847`, :spec:`aaf2434`)
   * :func:`~stem.process.launch_tor_with_config` avoids writing a temporary torrc to disk if able (:trac:`13865`)
   * :class:`~stem.response.events.CircuitEvent` support for the new SOCKS_USERNAME and SOCKS_PASSWORD arguments (:trac:`14555`, :spec:`2975974`)
   * The 'strict' argument of :func:`~stem.exit_policy.ExitPolicy.can_exit_to` didn't behave as documented (:trac:`14314`)
diff --git a/stem/control.py b/stem/control.py
index a688d21..48171d0 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -88,6 +88,7 @@ If you're fine with allowing your script to raise exceptions then this can be mo
     |- get_server_descriptors - provides all currently available server descriptors
     |- get_network_status - querying the router status entry for a relay
     |- get_network_statuses - provides all preently available router status entries
+    |- get_hidden_service_descriptor - queries the given hidden service descriptor
     |
     |- get_conf - gets the value of a configuration option
     |- get_conf_map - gets the values of multiple configuration options
@@ -1823,6 +1824,99 @@ class Controller(BaseController):
     for desc in desc_iterator:
       yield desc
 
+  @with_default()
+  def get_hidden_service_descriptor(self, address, default = UNDEFINED, servers = None, await_result = True):
+    """
+    get_hidden_service_descriptor(address, default = UNDEFINED, servers = None, await_result = True)
+
+    Provides the descriptor for a hidden service. The **address** is the
+    '.onion' address of the hidden service (for instance 3g2upl4pq6kufc4m.onion
+    for DuckDuckGo).
+
+    If **await_result** is **True** then this blocks until we either receive
+    the descriptor or the request fails. If **False** this returns right away.
+
+    .. versionadded:: 1.4.0
+
+    :param str address: address of the hidden service descriptor, the '.onion' suffix is optional
+    :param object default: response if the query fails
+    :param list servers: requrest the descriptor from these specific servers
+
+    :returns: :class:`~stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor`
+      for the given service if **await_result** is **True**, or **None** otherwise
+
+    :raises:
+      * :class:`stem.DescriptorUnavailable` if **await_result** is **True** and
+        unable to provide a descriptor for the given service
+      * :class:`stem.ControllerError` if unable to query the descriptor
+      * **ValueError** if **address** doesn't conform with the pattern of a
+        hidden service address
+
+      An exception is only raised if we weren't provided a default response.
+    """
+
+    if address.endswith('.onion'):
+      address = address[:-6]
+
+    if not stem.util.tor_tools.is_valid_hidden_service_address(address):
+      raise ValueError("'%s.onion' isn't a valid hidden service address" % address)
+
+    # TODO: Uncomment the below when tor makes its 0.2.7.1 release.
+    # if self.get_version() < stem.version.Requirement.HSFETCH:
+    #   raise stem.UnsatisfiableRequest(message = 'HSFETCH was added in tor version %s' % stem.version.Requirement.HSFETCH)
+
+    hs_desc_queue, hs_desc_listener = queue.Queue(), None
+    hs_desc_content_queue, hs_desc_content_listener = queue.Queue(), None
+
+    if await_result:
+      def hs_desc_listener(event):
+        hs_desc_queue.put(event)
+
+      def hs_desc_content_listener(event):
+        hs_desc_content_queue.put(event)
+
+      self.add_event_listener(hs_desc_listener, EventType.HS_DESC)
+      self.add_event_listener(hs_desc_content_listener, EventType.HS_DESC_CONTENT)
+
+    try:
+      request = 'HSFETCH %s' % address
+
+      if servers:
+        request += ' '.join(['SERVER=%s' % s for s in servers])
+
+      response = self.msg(request)
+      stem.response.convert('SINGLELINE', response)
+
+      if not response.is_ok():
+        raise stem.ProtocolError('HSFETCH returned unexpected response code: %s' % response.code)
+
+      if not await_result:
+        return None  # not waiting, so nothing to provide back
+      else:
+        while True:
+          event = hs_desc_content_queue.get()
+
+          if event.address == address:
+            if event.descriptor:
+              return event.descriptor
+            else:
+              # no descriptor, looking through HS_DESC to figure out why
+
+              while True:
+                event = hs_desc_queue.get()
+
+                if event.address == address and event.action == stem.HSDescAction.FAILED:
+                  if event.reason == stem.HSDescReason.NOT_FOUND:
+                    raise stem.DescriptorUnavailable('No running hidden service at %s.onion' % address)
+                  else:
+                    raise stem.DescriptorUnavailable('Unable to retrieve the descriptor for %s.onion (retrieved from %s): %s' % (address, event.directory_fingerprint, event.reason))
+    finally:
+      if hs_desc_listener:
+        self.remove_event_listener(hs_desc_listener)
+
+      if hs_desc_content_listener:
+        self.remove_event_listener(hs_desc_content_listener)
+
   def get_conf(self, param, default = UNDEFINED, multiple = False):
     """
     Queries the current value for a configuration option. Some configuration
@@ -2818,11 +2912,9 @@ class Controller(BaseController):
     # to build. This is icky, but we can't reliably do this via polling since
     # we then can't get the failure if it can't be created.
 
-    circ_queue, circ_listener = None, None
+    circ_queue, circ_listener = queue.Queue(), None
 
     if await_build:
-      circ_queue = queue.Queue()
-
       def circ_listener(event):
         circ_queue.put(event)
 
@@ -3152,7 +3244,7 @@ class Controller(BaseController):
     """
 
     if self.get_version() < stem.version.Requirement.DROPGUARDS:
-      raise stem.UnsatisfiableRequest('DROPGUARDS was added in tor version %s' % stem.version.Requirement.DROPGUARDS)
+      raise stem.UnsatisfiableRequest(message = 'DROPGUARDS was added in tor version %s' % stem.version.Requirement.DROPGUARDS)
 
     self.msg('DROPGUARDS')
 
diff --git a/stem/interpreter/settings.cfg b/stem/interpreter/settings.cfg
index 290e646..696ff3f 100644
--- a/stem/interpreter/settings.cfg
+++ b/stem/interpreter/settings.cfg
@@ -81,6 +81,7 @@ help.general
 |  ATTACHSTREAM - associates an application's stream with a tor circuit
 |  REDIRECTSTREAM - sets a stream's destination
 |  CLOSESTREAM - closes the given stream
+|  HSFETCH - retrieve a hidden service descriptor, providing it in a HS_DESC_CONTENT event
 |  RESOLVE - issues an asynchronous dns or rdns request over tor
 |  TAKEOWNERSHIP - instructs tor to quit when this control connection is closed
 |  PROTOCOLINFO - queries version and controller authentication information
@@ -112,6 +113,7 @@ help.usage CLOSECIRCUIT => CLOSECIRCUIT CircuitID [IfUnused]
 help.usage ATTACHSTREAM => ATTACHSTREAM StreamID CircuitID [HOP=HopNum]
 help.usage REDIRECTSTREAM => REDIRECTSTREAM StreamID Address [Port]
 help.usage CLOSESTREAM => CLOSESTREAM StreamID Reason [Flag]
+help.usage HSFETCH => HSFETCH (HSAddress/v2-DescId) [SERVER=Server]...
 help.usage RESOLVE => RESOLVE [mode=reverse] address
 help.usage TAKEOWNERSHIP => TAKEOWNERSHIP
 help.usage PROTOCOLINFO => PROTOCOLINFO [ProtocolVersion]
@@ -245,6 +247,10 @@ help.description.closestream
 |Closes the given stream, the reason being an integer matching a reason as
 |per section 6.3 of the tor-spec.
 
+help.description.hsfetch
+|Retrieves the descriptor for a hidden service. This is an asynchronous
+|request, with the descriptor provided by a HS_DESC_CONTENT event.
+
 help.description.resolve
 |Performs IPv4 DNS resolution over tor, doing a reverse lookup instead if
 |"mode=reverse" is included. This request is processed in the background and
diff --git a/stem/response/events.py b/stem/response/events.py
index 438d1e5..e03f269 100644
--- a/stem/response/events.py
+++ b/stem/response/events.py
@@ -674,7 +674,7 @@ class HSDescContentEvent(Event):
   :var stem.descriptor.hidden_service_descriptor.HiddenServiceDescriptor descriptor: descriptor that was retrieved
   """
 
-  # TODO: Double check that this version is correct when #14847 is merged, then add to stem.version.Requirement.
+  # TODO: Uncomment the below when tor makes its 0.2.7.1 release.
   # _VERSION_ADDED = stem.version.Requirement.EVENT_HS_DESC_CONTENT
   _POSITIONAL_ARGS = ('address', 'descriptor_id', 'directory')
 
diff --git a/stem/util/tor_tools.py b/stem/util/tor_tools.py
index 333fddc..01c29ee 100644
--- a/stem/util/tor_tools.py
+++ b/stem/util/tor_tools.py
@@ -15,6 +15,7 @@ Miscellaneous utility functions for working with tor.
   is_valid_circuit_id - checks if a string is a valid tor circuit id
   is_valid_stream_id - checks if a string is a valid tor stream id
   is_valid_connection_id - checks if a string is a valid tor connection id
+  is_valid_hidden_service_address - checks if a string is a valid hidden service address
   is_hex_digits - checks if a string is only made up of hex digits
 """
 
@@ -36,6 +37,10 @@ import re
 NICKNAME_PATTERN = re.compile('^[a-zA-Z0-9]{1,19}$')
 CIRC_ID_PATTERN = re.compile('^[a-zA-Z0-9]{1,16}$')
 
+# Hidden service addresses are sixteen base32 characters.
+
+HS_ADDRESS_PATTERN = re.compile('^[a-z2-7]{16}$')
+
 
 def is_valid_fingerprint(entry, check_prefix = False):
   """
@@ -111,6 +116,20 @@ def is_valid_connection_id(entry):
   return is_valid_circuit_id(entry)
 
 
+def is_valid_hidden_service_address(entry):
+  """
+  Checks if a string is a valid format for being a hidden service address (not
+  including the '.onion' suffix).
+
+  :returns: **True** if the string could be a hidden service address, **False** otherwise
+  """
+
+  try:
+    return bool(HS_ADDRESS_PATTERN.match(entry))
+  except TypeError:
+    return False
+
+
 def is_hex_digits(entry, count):
   """
   Checks if a string is the given number of hex digits. Digits represented by
diff --git a/stem/version.py b/stem/version.py
index 0e451b5..2289a3d 100644
--- a/stem/version.py
+++ b/stem/version.py
@@ -38,6 +38,7 @@ easily parsed and compared, for instance...
   **EVENT_CONF_CHANGED**                CONF_CHANGED events
   **EVENT_DESCCHANGED**                 DESCCHANGED events
   **EVENT_GUARD**                       GUARD events
+  **EVENT_HS_DESC_CONTENT**             HS_DESC_CONTENT events
   **EVENT_NEWCONSENSUS**                NEWCONSENSUS events
   **EVENT_NS**                          NS events
   **EVENT_SIGNAL**                      SIGNAL events
@@ -53,6 +54,7 @@ easily parsed and compared, for instance...
   **FEATURE_EXTENDED_EVENTS**           'EXTENDED_EVENTS' optional feature
   **FEATURE_VERBOSE_NAMES**             'VERBOSE_NAMES' optional feature
   **GETINFO_CONFIG_TEXT**               'GETINFO config-text' query
+  **HSFETCH**                           HSFETCH requests
   **LOADCONF**                          LOADCONF requests
   **MICRODESCRIPTOR_IS_DEFAULT**        Tor gets microdescriptors by default rather than server descriptors
   **TAKEOWNERSHIP**                     TAKEOWNERSHIP requests
@@ -343,6 +345,7 @@ Requirement = stem.util.enum.Enum(
   ('EVENT_CONF_CHANGED', Version('0.2.3.3-alpha')),
   ('EVENT_DESCCHANGED', Version('0.1.2.2-alpha')),
   ('EVENT_GUARD', Version('0.1.2.5-alpha')),
+  ('EVENT_HS_DESC_CONTENT', Version('0.2.7.1-alpha')),
   ('EVENT_NS', Version('0.1.2.3-alpha')),
   ('EVENT_NEWCONSENSUS', Version('0.2.1.13-alpha')),
   ('EVENT_SIGNAL', Version('0.2.3.1-alpha')),
@@ -358,6 +361,7 @@ Requirement = stem.util.enum.Enum(
   ('FEATURE_EXTENDED_EVENTS', Version('0.2.2.1-alpha')),
   ('FEATURE_VERBOSE_NAMES', Version('0.2.2.1-alpha')),
   ('GETINFO_CONFIG_TEXT', Version('0.2.2.7-alpha')),
+  ('HSFETCH', Version('0.2.7.1-alpha')),
   ('LOADCONF', Version('0.2.1.1')),
   ('MICRODESCRIPTOR_IS_DEFAULT', Version('0.2.3.3')),
   ('TAKEOWNERSHIP', Version('0.2.2.28-beta')),
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index f5eef33..01480ba 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -1130,6 +1130,37 @@ class TestController(unittest.TestCase):
         if count > 10:
           break
 
+  # TODO: Uncomment the below when tor makes its 0.2.7.1 release.
+  # @require_version(Requirement.HSFETCH)
+
+  @require_controller
+  @require_online
+  def test_get_hidden_service_descriptor(self):
+    """
+    Fetches a few descriptors via the get_hidden_service_descriptor() method.
+    """
+
+    runner = test.runner.get_runner()
+
+    with runner.get_tor_controller() as controller:
+      # fetch the descriptor for DuckDuckGo
+
+      desc = controller.get_hidden_service_descriptor('3g2upl4pq6kufc4m.onion')
+      self.assertTrue('MIGJAoGBAJ' in desc.permanent_key)
+
+      # try to fetch something that doesn't exist
+
+      try:
+        desc = controller.get_hidden_service_descriptor('m4cfuk6qp4lpu2g3')
+        self.fail("Didn't expect m4cfuk6qp4lpu2g3.onion to exist, but provided: %s" % desc)
+      except stem.DescriptorUnavailable as exc:
+        self.assertEqual('No running hidden service at m4cfuk6qp4lpu2g3.onion', str(exc))
+
+      # ... but shouldn't fail if we have a default argument or aren't awaiting the descriptor
+
+      self.assertEqual('pop goes the weasel', controller.get_hidden_service_descriptor('m4cfuk6qp4lpu2g5', 'pop goes the weasel'))
+      self.assertEqual(None, controller.get_hidden_service_descriptor('m4cfuk6qp4lpu2g5', await_result = False))
+
   @require_controller
   @require_online
   @require_version(Requirement.EXTENDCIRCUIT_PATH_OPTIONAL)
diff --git a/test/unit/response/events.py b/test/unit/response/events.py
index 756e531..e5ddc1a 100644
--- a/test/unit/response/events.py
+++ b/test/unit/response/events.py
@@ -206,7 +206,7 @@ HS_DESC_FAILED = '650 HS_DESC FAILED ajhb7kljbiru65qo NO_AUTH \
 $67B2BDA4264D8A189D9270E28B1D30A262838243 \
 b3oeducbhjmbqmgw2i3jtz4fekkrinwj REASON=NOT_FOUND'
 
-# HS_DESC_CONTENT is unreleased (plan is tor 0.2.7.1-alpha, #14847)
+# HS_DESC_CONTENT from 0.2.7.1
 
 HS_DESC_CONTENT_EVENT = """\
 650+HS_DESC_CONTENT facebookcorewwwi riwvyw6njgvs4koel4heqs7w4bssnmlw $8A30C9E8F5954EE286D29BD65CADEA6991200804~YorkshireTOR



More information about the tor-commits mailing list