[tor-commits] [stem/master] Better support for multiple hidden services

atagar at torproject.org atagar at torproject.org
Tue Oct 28 17:11:05 UTC 2014


commit 1294934aa8b2d0bf6bf25ed081ddc54537fad939
Author: Patrick O'Doherty <p at trickod.com>
Date:   Mon Oct 27 19:56:04 2014 -0700

    Better support for multiple hidden services
    
    Adds the following Controller methods for manipulating hidden services.
     * get_hidden_services_conf
     * set_hidden_services_conf
     * create_new_hidden_service
     * delete_hidden_service
    
    Fixes issue #12533 where creating new hidden service would clobber the
    configuration for existing services
---
 stem/control.py                  |  174 ++++++++++++++++++++++++++++++++++++++
 test/integ/control/controller.py |   73 ++++++++++++++++
 2 files changed, 247 insertions(+)

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





More information about the tor-commits mailing list