Skip to content

Instantly share code, notes, and snippets.

@patrickod
Last active August 29, 2015 14:08
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save patrickod/3ea33c76f903ced87f80 to your computer and use it in GitHub Desktop.
Patch fixing issue #12533 in Stem, the python controller library for the Tor project. https://trac.torproject.org/projects/tor/ticket/12533
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