commit 8a90aa4d0b67efd40c911caf53b15f73b1b94b70
Author: Damian Johnson <atagar(a)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