Patch fixing issue #12533 in Stem, the python controller library for the Tor project. https://trac.torproject.org/projects/tor/ticket/12533
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/stem/control.py b/stem/control.py | |
index ed83a00..1282eb3 100644 | |
--- a/stem/control.py | |
+++ b/stem/control.py | |
@@ -224,6 +224,12 @@ import StringIO | |
import threading | |
import time | |
+try: | |
+ # Added in 2.7 | |
+ from collections import OrderedDict | |
+except ImportError: | |
+ from stem.util.ordereddict import OrderedDict | |
+ | |
import stem.descriptor.microdescriptor | |
import stem.descriptor.reader | |
import stem.descriptor.router_status_entry | |
@@ -1888,6 +1894,174 @@ class Controller(BaseController): | |
else: | |
raise exc | |
+ def get_hidden_services_conf(self, default = UNDEFINED): | |
+ """ | |
+ This provides a mapping of hidden service directories to their | |
+ attribute's key/value pairs. | |
+ | |
+ { | |
+ "/var/lib/tor/hidden_service_empty/": { | |
+ "HiddenServicePort": [ | |
+ ] | |
+ }, | |
+ "/var/lib/tor/hidden_service_with_two_ports/": { | |
+ "HiddenServiceAuthorizeClient": "stealth a, b", | |
+ "HiddenServicePort": [ | |
+ "8020 127.0.0.1:8020", # the ports order is kept | |
+ "8021 127.0.0.1:8021" | |
+ ], | |
+ "HiddenServiceVersion": "2" | |
+ }, | |
+ } | |
+ | |
+ :raises: | |
+ * :class:`stem.ControllerError` if the call fails and we weren't provided | |
+ a default response | |
+ """ | |
+ start_time = time.time() | |
+ | |
+ try: | |
+ response = self.msg('GETCONF HiddenServiceOptions') | |
+ stem.response.convert('GETCONF', response) | |
+ log.debug('GETCONF HiddenServiceOptions (runtime: %0.4f)' % | |
+ (time.time() - start_time)) | |
+ except stem.ControllerError as exc: | |
+ log.debug('GETCONF HiddenServiceOptions (failed: %s)' % exc) | |
+ if default != UNDEFINED: | |
+ return default | |
+ else: | |
+ raise exc | |
+ | |
+ service_dir_map = OrderedDict() | |
+ directory = None | |
+ | |
+ for status_code, divider, content in response.content(): | |
+ if content == 'HiddenServiceOptions': | |
+ continue | |
+ | |
+ if not "=" in content: | |
+ continue | |
+ | |
+ k, v = content.split('=', 1) | |
+ | |
+ if k == 'HiddenServiceDir': | |
+ directory = v | |
+ service_dir_map[directory] = {'HiddenServicePort': []} | |
+ | |
+ elif k == 'HiddenServicePort': | |
+ service_dir_map[directory]['HiddenServicePort'].append(v) | |
+ | |
+ else: | |
+ service_dir_map[directory][k] = v | |
+ | |
+ return service_dir_map | |
+ | |
+ def set_hidden_services_conf(self, conf): | |
+ """Update all the configured hidden services from a dictionary having | |
+ the same format as the output of get_hidden_services_conf() | |
+ | |
+ :param dict conf: configuration dictionary | |
+ | |
+ :raises: | |
+ * :class:`stem.ControllerError` if the call fails | |
+ * :class:`stem.InvalidArguments` if configuration options | |
+ requested was invalid | |
+ * :class:`stem.InvalidRequest` if the configuration setting is | |
+ impossible or if there's a syntax error in the configuration values | |
+ * :raises: | |
+ """ | |
+ | |
+ # Convert conf dictionary into a list of ordered config tuples | |
+ hidden_service_options = [] | |
+ for directory in conf: | |
+ hidden_service_options.append(('HiddenServiceDir', directory)) | |
+ for k, v in conf[directory].iteritems(): | |
+ if k == 'HiddenServicePort': | |
+ for port in v: | |
+ hidden_service_options.append(('HiddenServicePort', port)) | |
+ else: | |
+ hidden_service_options.append((k, str(v))) | |
+ self.set_options(hidden_service_options) | |
+ | |
+ def create_new_hidden_service(self, dirname, virtport, target=None): | |
+ """Create a new hidden service+port. If the directory is already present, a | |
+ new port will be added. If the port is already present, return False. | |
+ | |
+ :param str dirname: directory name | |
+ :param int virtport: virtual port | |
+ :param str target: optional ipaddr:port target e.g. '127.0.0.1:8080' | |
+ :returns: False if the hidden service and port is already in place | |
+ True if the creation is successful | |
+ """ | |
+ if stem.util.connection.is_valid_port(virtport): | |
+ virtport = int(virtport) | |
+ | |
+ else: | |
+ raise ValueError("%s isn't a valid port number" % virtport) | |
+ | |
+ conf = self.get_hidden_services_conf() | |
+ | |
+ if dirname in conf: | |
+ ports = conf[dirname]['HiddenServicePort'] | |
+ if target is None: | |
+ if str(virtport) in ports: | |
+ return False | |
+ | |
+ if "%d 127.0.0.1:%d" % (virtport, virtport) in ports: | |
+ return False | |
+ | |
+ elif "%d %s" % (virtport, target) in ports: | |
+ return False | |
+ | |
+ else: | |
+ conf[dirname] = {'HiddenServicePort': []} | |
+ | |
+ if target is None: | |
+ conf[dirname]['HiddenServicePort'].append("%d" % virtport) | |
+ | |
+ else: | |
+ conf[dirname]['HiddenServicePort'].append("%d %s" % (virtport, target)) | |
+ | |
+ self.set_hidden_services_conf(conf) | |
+ return True | |
+ | |
+ def delete_hidden_service(self, dirname, virtport, target=None): | |
+ """Delete a hidden service+port. | |
+ :param str dirname: directory name | |
+ :param int virtport: virtual port | |
+ :param str target: optional ipaddr:port target e.g. '127.0.0.1:8080' | |
+ :raises: | |
+ """ | |
+ if stem.util.connection.is_valid_port(virtport): | |
+ virtport = int(virtport) | |
+ | |
+ else: | |
+ raise ValueError("%s isn't a valid port number" % virtport) | |
+ | |
+ conf = self.get_hidden_services_conf() | |
+ | |
+ if dirname not in conf: | |
+ raise RuntimeError("HiddenServiceDir %r not found" % dirname) | |
+ | |
+ ports = conf[dirname]['HiddenServicePort'] | |
+ | |
+ if target is None: | |
+ longport = "%d 127.0.0.1:%d" % (virtport, virtport) | |
+ try: | |
+ ports.pop(ports.index(str(virtport))) | |
+ except ValueError: | |
+ raise stem.InvalidArguments | |
+ | |
+ else: | |
+ longport = "%d %s" % (virtport, target) | |
+ ports.pop(ports.index(longport)) | |
+ | |
+ if not ports: | |
+ del(conf[dirname]) | |
+ | |
+ self.set_hidden_services_conf(conf) | |
+ return True | |
+ | |
def _get_conf_dict_to_response(self, config_dict, default, multiple): | |
""" | |
Translates a dictionary of 'config key => [value1, value2...]' into the | |
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py | |
index 05ce3f7..4935043 100644 | |
--- a/test/integ/control/controller.py | |
+++ b/test/integ/control/controller.py | |
@@ -455,6 +455,79 @@ class TestController(unittest.TestCase): | |
self.assertEqual({}, controller.get_conf_map('', 'la-di-dah')) | |
self.assertEqual({}, controller.get_conf_map([], 'la-di-dah')) | |
+ def test_hidden_services_conf(self): | |
+ """ | |
+ Exercises get_hidden_services_conf with valid and invalid queries. | |
+ """ | |
+ | |
+ if test.runner.require_control(self): | |
+ return | |
+ | |
+ runner = test.runner.get_runner() | |
+ | |
+ with runner.get_tor_controller() as controller: | |
+ | |
+ conf = controller.get_hidden_services_conf() | |
+ self.assertDictEqual({}, conf) | |
+ controller.set_hidden_services_conf(conf) | |
+ | |
+ initialconf = { | |
+ "test_hidden_service1/": { | |
+ "HiddenServicePort": [ | |
+ "8020 127.0.0.1:8020", | |
+ "8021 127.0.0.1:8021" | |
+ ], | |
+ "HiddenServiceVersion": "2", | |
+ }, | |
+ "test_hidden_service2/": { | |
+ "HiddenServiceAuthorizeClient": "stealth a, b", | |
+ "HiddenServicePort": [ | |
+ "8030 127.0.0.1:8030", | |
+ "8031 127.0.0.1:8031", | |
+ "8032 127.0.0.1:8032" | |
+ ] | |
+ }, | |
+ "test_hidden_service_empty/": { | |
+ "HiddenServicePort": [] | |
+ } | |
+ } | |
+ controller.set_hidden_services_conf(initialconf) | |
+ | |
+ conf = controller.get_hidden_services_conf() | |
+ self.assertDictEqual(initialconf, dict(conf)) | |
+ | |
+ # Add already existing services, with/without explicit target | |
+ r = controller.create_new_hidden_service('test_hidden_service1/', 8020) | |
+ self.assertFalse(r) | |
+ r = controller.create_new_hidden_service('test_hidden_service1/', 8021, target="127.0.0.1:8021") | |
+ self.assertFalse(r) | |
+ | |
+ # Add new services, with/without explicit target | |
+ r = controller.create_new_hidden_service('test_hidden_serviceX/', 8888) | |
+ self.assertTrue(r) | |
+ r = controller.create_new_hidden_service('test_hidden_serviceX/', 8989, target="127.0.0.1:8021") | |
+ self.assertTrue(r) | |
+ | |
+ conf = controller.get_hidden_services_conf() | |
+ self.assertEqual(len(conf), 4) | |
+ ports = conf['test_hidden_serviceX/']['HiddenServicePort'] | |
+ self.assertEqual(len(ports), 2) | |
+ | |
+ # Delete services | |
+ controller.delete_hidden_service('test_hidden_serviceX/', 8888) | |
+ | |
+ # The service dir should be still there | |
+ conf = controller.get_hidden_services_conf() | |
+ self.assertEqual(len(conf), 4) | |
+ | |
+ # Delete service | |
+ controller.delete_hidden_service('test_hidden_serviceX/', 8989, target="127.0.0.1:8021") | |
+ | |
+ # The service dir should be gone | |
+ conf = controller.get_hidden_services_conf() | |
+ self.assertEqual(len(conf), 3) | |
+ | |
+ | |
def test_set_conf(self): | |
""" | |
Exercises set_conf(), reset_conf(), and set_options() methods with valid | |
diff --git a/test/unit/util/system.py b/test/unit/util/system.py | |
index 911f151..40dd652 100644 | |
--- a/test/unit/util/system.py | |
+++ b/test/unit/util/system.py | |
@@ -75,7 +75,7 @@ def mock_call(base_cmd, responses): | |
ways of using this... | |
- Simple usage is for base_cmd is the system call we want to respond to and | |
- responses is a list containing the respnose. For instance... | |
+ responses is a list containing the response. For instance... | |
mock_call('ls my_dir', ['file1', 'file2', 'file3']) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment