commit 2446babb8d03fb58d44ccec7cb333afd472c9395 Author: Damian Johnson atagar@torproject.org Date: Tue Oct 28 10:07:42 2014 -0700
Revisions for new hidden service methods
Lots of changes for the patch from...
https://trac.torproject.org/projects/tor/ticket/12533
Some that come to mind...
* Revised pydocs, added header entries, and updated the changelog.
* Reordered the new methods to be in their own grouping.
* Renamed get_hidden_services_conf and set_hidden_services_conf to drop the plural.
* Renamed deleted_hidden_service to remove_hidden_service (since we're not actually deleting anything).
* Making create_hidden_service and remove_hidden_service be idempotent, returning a boolean to indicate if they did anything or not.
* Dropping hidden services after our integ tests are done. --- docs/change_log.rst | 1 + stem/control.py | 357 ++++++++++++++++++++------------------ test/integ/control/controller.py | 106 +++++------ 3 files changed, 244 insertions(+), 220 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index 7297cb9..369806a 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -42,6 +42,7 @@ The following are only available within Stem's `git repository
* **Controller**
+ * Added :class:`~stem.control.Controller` methods to more easily work with hidden service configurations: :func:`~stem.control.Controller.get_hidden_service_conf`, :func:`~stem.control.Controller.set_hidden_service_conf`, :func:`~stem.control.Controller.create_hidden_service`, and :func:`~stem.control.Controller.remove_hidden_service` (feature by federico3 and patrickod, :trac:`12533`) * Added :func:`~stem.control.Controller.get_accounting_stats` to the :class:`~stem.control.Controller` * Added :func:`~stem.control.Controller.get_effective_rate` to the :class:`~stem.control.Controller` * Added :func:`~stem.control.BaseController.connection_time` to the :class:`~stem.control.BaseController` diff --git a/stem/control.py b/stem/control.py index 1282eb3..93fe354 100644 --- a/stem/control.py +++ b/stem/control.py @@ -95,6 +95,11 @@ If you're fine with allowing your script to raise exceptions then this can be mo |- reset_conf - reverts configuration options to their default values |- set_options - sets or resets the values of multiple configuration options | + |- get_hidden_service_conf - provides our hidden service configuration + |- set_hidden_service_conf - sets our hidden service configuration + |- create_hidden_service - creates a new hidden service or adds a new port + |- remove_hidden_service - removes a hidden service or drops a port + | |- add_event_listener - attaches an event listener to be notified of tor events |- remove_event_listener - removes a listener so it isn't notified of further events | @@ -1894,174 +1899,6 @@ 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 @@ -2205,6 +2042,190 @@ class Controller(BaseController): else: raise stem.ProtocolError('Returned unexpected status code: %s' % response.code)
+ def get_hidden_service_conf(self, default = UNDEFINED): + """ + This provides a mapping of hidden service directories to their + attribute's key/value pairs. All hidden services are assured to have a + 'HiddenServicePort', but other entries may or may not exist. + + :: + + { + "/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" + }, + } + + :param object default: response if the query fails + + :returns: **dict** with the hidden service configuration + + :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_service_conf(self, conf): + """ + Update all the configured hidden services from a dictionary having + the same format as + :func:`~stem.control.Controller.get_hidden_service_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 + """ + + # 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].items(): + 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_hidden_service(self, path, port, target = None): + """ + Create a new hidden service. If the directory is already present, a + new port is added. + + :param str path: path for the hidden service's data directory + :param int port: hidden service port + :param str target: optional ipaddr:port target e.g. '127.0.0.1:8080' + + :returns: **True** if the hidden service is created, **False** if the + hidden service port is already in use + + :raises: :class:`stem.ControllerError` if the call fails + """ + + if not stem.util.connection.is_valid_port(port): + raise ValueError("%s isn't a valid port number" % port) + + port, conf = str(port), self.get_hidden_service_conf() + + if path in conf: + ports = conf[path]['HiddenServicePort'] + + if target is None: + if port in ports or '%s 127.0.0.1:%s' % (port, port) in ports: + return False + elif '%s %s' % (port, target) in ports: + return False + else: + conf[path] = {'HiddenServicePort': []} + + conf[path]['HiddenServicePort'].append('%s %s' % (port, target) if target else port) + self.set_hidden_service_conf(conf) + + return True + + def remove_hidden_service(self, path, port = None, target = None): + """ + Discontinues a given hidden service. + + :param str path: path for the hidden service's data directory + :param int port: hidden service port + :param str target: optional ipaddr:port target e.g. '127.0.0.1:8080' + + :returns: **True** if the hidden service is discontinued, **False** if it + wasn't running in the first place + + :raises: :class:`stem.ControllerError` if the call fails + """ + + if not stem.util.connection.is_valid_port(port): + raise ValueError("%s isn't a valid port number" % port) + + port, conf = str(port), self.get_hidden_service_conf() + + if path not in conf: + return False + + if port: + if not target: + longport = '%s 127.0.0.1:%s' % (port, port) + + if port in conf[path]['HiddenServicePort']: + conf[path]['HiddenServicePort'].remove(port) + elif longport in conf[path]['HiddenServicePort']: + conf[path]['HiddenServicePort'].remove(longport) + else: + return False # wasn't configured to be a hidden service + else: + longport = '%s %s' % (port, target) + + if longport in conf[path]['HiddenServicePort']: + conf[path]['HiddenServicePort'].remove(longport) + else: + return False # wasn't configured to be a hidden service + + if not conf[path]['HiddenServicePort']: + del(conf[path]) # no ports left, drop it entirely + else: + del(conf[path]) + + self.set_hidden_service_conf(conf) + return True + def add_event_listener(self, listener, *events): """ Directs further tor controller events to a given function. The function is diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index 4935043..1f20f9f 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -457,7 +457,8 @@ class TestController(unittest.TestCase):
def test_hidden_services_conf(self): """ - Exercises get_hidden_services_conf with valid and invalid queries. + Exercises the hidden service family of methods (get_hidden_service_conf, + set_hidden_service_conf, create_hidden_service, and remove_hidden_service). """
if test.runner.require_control(self): @@ -466,67 +467,68 @@ class TestController(unittest.TestCase): 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": [] + try: + # initially we shouldn't be running any hidden services + + self.assertDictEqual({}, controller.get_hidden_service_conf()) + + # try setting a blank config, shouldn't have any impact + + controller.set_hidden_service_conf({}) + self.assertDictEqual({}, controller.get_hidden_service_conf()) + + # create a hidden service + + 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)) + controller.set_hidden_service_conf(initialconf) + self.assertDictEqual(initialconf, controller.get_hidden_service_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 already existing services, with/without explicit target
- # 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) + self.assertFalse(controller.create_hidden_service('test_hidden_service1/', 8020)) + self.assertFalse(controller.create_hidden_service('test_hidden_service1/', 8021, target = '127.0.0.1:8021')) + self.assertDictEqual(initialconf, controller.get_hidden_service_conf())
- conf = controller.get_hidden_services_conf() - self.assertEqual(len(conf), 4) - ports = conf['test_hidden_serviceX/']['HiddenServicePort'] - self.assertEqual(len(ports), 2) + # add a new service, with/without explicit target
- # Delete services - controller.delete_hidden_service('test_hidden_serviceX/', 8888) + self.assertTrue(controller.create_hidden_service('test_hidden_serviceX/', 8888)) + self.assertTrue(controller.create_hidden_service('test_hidden_serviceX/', 8989, target = '127.0.0.1:8021'))
- # The service dir should be still there - conf = controller.get_hidden_services_conf() - self.assertEqual(len(conf), 4) + conf = controller.get_hidden_service_conf() + self.assertEqual(4, len(conf)) + self.assertEqual(2, len(conf['test_hidden_serviceX/']['HiddenServicePort']))
- # Delete service - controller.delete_hidden_service('test_hidden_serviceX/', 8989, target="127.0.0.1:8021") + # remove a hidden service, the service dir should still be there
- # The service dir should be gone - conf = controller.get_hidden_services_conf() - self.assertEqual(len(conf), 3) + controller.remove_hidden_service('test_hidden_serviceX/', 8888) + self.assertEqual(4, len(controller.get_hidden_service_conf()))
+ # remove a service completely, it should now be gone + + controller.remove_hidden_service('test_hidden_serviceX/', 8989, target = '127.0.0.1:8021') + self.assertEqual(3, len(controller.get_hidden_service_conf())) + finally: + controller.set_hidden_service_conf({}) # drop hidden services created during the test
def test_set_conf(self): """