commit b4402bfb6965106e8ab43612b641d65905e09603 Author: Damian Johnson atagar@torproject.org Date: Thu Apr 23 09:50:16 2015 -0700
Add auth_type and client_names arguments to create_hidden_service()
Support for specifying authentication and clients when making hidden services. This is a tweaked version of a patch from federico3 on...
https://trac.torproject.org/projects/tor/ticket/14320 --- docs/change_log.rst | 1 + stem/control.py | 52 ++++++++++++++++++++++++++++++-------- test/integ/control/controller.py | 22 +++++++++++++++- 3 files changed, 64 insertions(+), 11 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index 4c96528..47f2a49 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -55,6 +55,7 @@ conversion (:trac:`14075`). * :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`) * Threads spawned for status change listeners were never joined on, potentially causing noise during interpreter shutdown + * Added support for specifying the authentication type and client names in :func:`~stem.control.Controller.create_hidden_service` (:trac:`14320`)
* **Descriptors**
diff --git a/stem/control.py b/stem/control.py index 69b326d..a688d21 100644 --- a/stem/control.py +++ b/stem/control.py @@ -376,6 +376,7 @@ AccountingStats = collections.namedtuple('AccountingStats', [ CreateHiddenServiceOutput = collections.namedtuple('CreateHiddenServiceOutput', [ 'path', 'hostname', + 'hostname_for_client', 'config', ])
@@ -2279,15 +2280,20 @@ class Controller(BaseController):
self.set_options(hidden_service_options)
- def create_hidden_service(self, path, port, target_address = None, target_port = None): + def create_hidden_service(self, path, port, target_address = None, target_port = None, auth_type = None, client_names = None): """ Create a new hidden service. If the directory is already present, a new port is added. This provides a **namedtuple** of the following...
* path (str) - hidden service directory
- * hostname (str) - onion address of the service, this is only retrieved - if we can read the hidden service directory + * hostname (str) - Content of the hostname file, if no **client_names** + are provided this is the onion address of the service. This is only + retrieved if we can read the hidden service directory. + + * hostname_for_client (dict) - mapping of client names to their onion + address, this is only set if the **client_names** was provided and we + can read the hidden service directory
* config (dict) - tor's new hidden service configuration
@@ -2297,11 +2303,16 @@ class Controller(BaseController):
.. versionadded:: 1.3.0
+ .. versionchanged:: 1.4.0 + Added the auth_type and client_names arguments. + :param str path: path for the hidden service's data directory :param int port: hidden service port :param str target_address: address of the service, by default 127.0.0.1 :param int target_port: port of the service, by default this is the same as **port** + :param str auth_type: authentication type: basic, stealth or None to disable auth + :param list client_names: client names (1-16 characters "A-Za-z0-9+-_")
:returns: **CreateHiddenServiceOutput** if we create or update a hidden service, **None** otherwise
@@ -2314,6 +2325,8 @@ class Controller(BaseController): raise ValueError("%s isn't a valid IPv4 address" % target_address) elif target_port is not None and not stem.util.connection.is_valid_port(target_port): raise ValueError("%s isn't a valid port number" % target_port) + elif auth_type not in (None, 'basic', 'stealth'): + raise ValueError("%s isn't a recognized type of authentication" % auth_type)
port = int(port) target_address = target_address if target_address else '127.0.0.1' @@ -2325,9 +2338,14 @@ class Controller(BaseController): return None
conf.setdefault(path, OrderedDict()).setdefault('HiddenServicePort', []).append((port, target_address, target_port)) + + if auth_type and client_names: + hsac = "%s %s" % (auth_type, ','.join(client_names)) + conf[path]['HiddenServiceAuthorizeClient'] = hsac + self.set_hidden_service_conf(conf)
- hostname = None + hostname, hostname_for_client = None, {}
if self.is_localhost(): hostname_path = os.path.join(path, 'hostname') @@ -2349,16 +2367,30 @@ class Controller(BaseController): else: time.sleep(0.05)
- if os.path.exists(hostname_path): - try: - with open(hostname_path) as hostname_file: - hostname = hostname_file.read().strip() - except: - pass + try: + with open(hostname_path) as hostname_file: + hostname = hostname_file.read().strip() + + if client_names and '\n' in hostname: + # When there's multiple clients this looks like... + # + # ndisjxzkgcdhrwqf.onion sjUwjTSPznqWLdOPuwRUzg # client: c1 + # ndisjxzkgcdhrwqf.onion sUu92axuL5bKnA76s2KRfw # client: c2 + + for line in hostname.splitlines(): + if ' # client: ' in line: + address = line.split()[0] + client = line.split(' # client: ', 1)[1] + + if len(address) == 22 and address.endswith('.onion'): + hostname_for_client[client] = address + except: + pass
return CreateHiddenServiceOutput( path = path, hostname = hostname, + hostname_for_client = hostname_for_client, config = conf, )
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index 2265cb2..f5eef33 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -480,6 +480,7 @@ class TestController(unittest.TestCase): service1_path = os.path.join(test_dir, 'test_hidden_service1') service2_path = os.path.join(test_dir, 'test_hidden_service2') service3_path = os.path.join(test_dir, 'test_hidden_service3') + service4_path = os.path.join(test_dir, 'test_hidden_service4') empty_service_path = os.path.join(test_dir, 'test_hidden_service_empty')
with runner.get_tor_controller() as controller: @@ -547,12 +548,31 @@ class TestController(unittest.TestCase):
controller.remove_hidden_service(hs_path, 8989) self.assertEqual(3, len(controller.get_hidden_service_conf())) + + # add a new service, this time with client authentication + + hs_path = os.path.join(os.getcwd(), service4_path) + hs_attributes = controller.create_hidden_service(hs_path, 8888, auth_type = 'basic', client_names = ['c1', 'c2']) + + self.assertEqual(2, len(hs_attributes.hostname.splitlines())) + self.assertEqual(2, len(hs_attributes.hostname_for_client)) + self.assertTrue(hs_attributes.hostname_for_client['c1'].endswith('.onion')) + self.assertTrue(hs_attributes.hostname_for_client['c2'].endswith('.onion')) + + conf = controller.get_hidden_service_conf() + self.assertEqual(4, len(conf)) + self.assertEqual(1, len(conf[hs_path]['HiddenServicePort'])) + + # remove a hidden service + + controller.remove_hidden_service(hs_path, 8888) + self.assertEqual(3, len(controller.get_hidden_service_conf())) finally: controller.set_hidden_service_conf({}) # drop hidden services created during the test
# clean up the hidden service directories created as part of this test
- for path in (service1_path, service2_path, service3_path): + for path in (service1_path, service2_path, service3_path, service4_path): try: shutil.rmtree(path) except: