commit 8a90aa4d0b67efd40c911caf53b15f73b1b94b70 Author: Damian Johnson atagar@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
tor-commits@lists.torproject.org