commit 38aa76c0216e083b4eb5eb6cfc1deba03cf44988 Author: Damian Johnson atagar@torproject.org Date: Thu Jun 16 10:03:26 2016 -0700
Basic auth support for ADD_ONION
Support for basic authentiction special recently added to ADD_ONION...
https://gitweb.torproject.org/torspec.git/commit/?id=c2865d9 --- docs/change_log.rst | 1 + stem/control.py | 42 +++++++++++++++++++++++++++++++++++++++- stem/response/add_onion.py | 9 +++++++++ stem/version.py | 2 ++ test/integ/control/controller.py | 21 ++++++++++++++++++++ test/unit/doctest.py | 11 +++++++++++ test/unit/response/add_onion.py | 20 +++++++++++++++++++ 7 files changed, 105 insertions(+), 1 deletion(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst index ecfc49a..291bca2 100644 --- a/docs/change_log.rst +++ b/docs/change_log.rst @@ -49,6 +49,7 @@ The following are only available within Stem's `git repository * :func:`~stem.connection.connect` and :func:`~stem.control.Controller.from_port` now connect to both port 9051 (relay's default) and 9151 (Tor Browser's default) (:trac:`16075`) * :class:`~stem.exit_policy.ExitPolicy` support for *accept6/reject6* and **4/6* wildcards (:trac:`16053`) * Added `support for NETWORK_LIVENESS events <api/response.html#stem.response.events.NetworkLivenessEvent>`_ (:spec:`44aac63`) + * Added support for basic authentication to :func:`~stem.control.Controller.create_ephemeral_hidden_service` (:spec:`c2865d9`) * Added :func:`~stem.control.event_description` for getting human-friendly descriptions of tor events (:trac:`19061`) * Added :func:`~stem.control.Controller.reconnect` to the :class:`~stem.control.Controller` * Added :func:`~stem.control.Controller.is_set` to the :class:`~stem.control.Controller` diff --git a/stem/control.py b/stem/control.py index 31e19d5..f29fb71 100644 --- a/stem/control.py +++ b/stem/control.py @@ -2764,7 +2764,7 @@ class Controller(BaseController):
return result
- def create_ephemeral_hidden_service(self, ports, key_type = 'NEW', key_content = 'BEST', discard_key = False, detached = False, await_publication = False): + def create_ephemeral_hidden_service(self, ports, key_type = 'NEW', key_content = 'BEST', discard_key = False, detached = False, await_publication = False, basic_auth = None): """ Creates a new hidden service. Unlike :func:`~stem.control.Controller.create_hidden_service` this style of @@ -2789,8 +2789,34 @@ class Controller(BaseController):
create_ephemeral_hidden_service({80: 80, 443: '173.194.33.133:443'})
+ If **basic_auth** is provided this service will require basic + authentication to access. This means users must set HidServAuth in their + torrc with credentials to access it. + + **basic_auth** is a mapping of usernames to their credentials. If the + credential is **None** one is generated and returned as part of the + response. For instance, only bob can access using the given newly generated + credentials... + + :: + + >>> response = controller.create_ephemeral_hidden_service(80, basic_auth = {'bob': None}) + >>> print(response.client_auth) + {'bob': 'nKwfvVPmTNr2k2pG0pzV4g'} + + ... while both alice and bob can access with existing credentials in the + following... + + controller.create_ephemeral_hidden_service(80, basic_auth = { + 'alice': 'l4BT016McqV2Oail+Bwe6w', + 'bob': 'vGnNRpWYiMBFTWD2gbBlcA', + }) + .. versionadded:: 1.4.0
+ .. versionchanged:: 1.5.0 + Added the basic_auth argument. + :param int,list,dict ports: hidden service port(s) or mapping of hidden service ports to their targets :param str key_type: type of key being provided, generates a new key if @@ -2803,6 +2829,7 @@ class Controller(BaseController): connection is closed if **True** :param bool await_publication: blocks until our descriptor is successfully published if **True** + :param dict basic_auth: required user credentials to access this service
:returns: :class:`~stem.response.add_onion.AddOnionResponse` with the response
@@ -2830,6 +2857,12 @@ class Controller(BaseController): if detached: flags.append('Detach')
+ if basic_auth is not None: + if self.get_version() < stem.version.Requirement.ADD_ONION_BASIC_AUTH: + raise stem.UnsatisfiableRequest(message = 'Basic authentication support was added to ADD_ONION in tor version %s' % stem.version.Requirement.ADD_ONION_BASIC_AUTH) + + flags.append('BasicAuth') + if flags: request += ' Flags=%s' % ','.join(flags)
@@ -2844,6 +2877,13 @@ class Controller(BaseController): else: raise ValueError("The 'ports' argument of create_ephemeral_hidden_service() needs to be an int, list, or dict")
+ if basic_auth is not None: + for client_name, client_blob in basic_auth.items(): + if client_blob: + request += ' ClientAuth=%s:%s' % (client_name, client_blob) + else: + request += ' ClientAuth=%s' % client_name + response = self.msg(request) stem.response.convert('ADD_ONION', response)
diff --git a/stem/response/add_onion.py b/stem/response/add_onion.py index 2b87755..cbf7594 100644 --- a/stem/response/add_onion.py +++ b/stem/response/add_onion.py @@ -12,17 +12,20 @@ class AddOnionResponse(stem.response.ControlMessage): :var str private_key: base64 encoded hidden service private key :var str private_key_type: crypto used to generate the hidden service private key (such as RSA1024) + :var dict client_auth: newly generated client credentials the service accepts """
def _parse_message(self): # Example: # 250-ServiceID=gfzprpioee3hoppz # 250-PrivateKey=RSA1024:MIICXgIBAAKBgQDZvYVxv... + # 250-ClientAuth=bob:l4BT016McqV2Oail+Bwe6w # 250 OK
self.service_id = None self.private_key = None self.private_key_type = None + self.client_auth = {}
if not self.is_ok(): raise stem.ProtocolError("ADD_ONION response didn't have an OK status: %s" % self) @@ -41,3 +44,9 @@ class AddOnionResponse(stem.response.ControlMessage): raise stem.ProtocolError("ADD_ONION PrivateKey lines should be of the form 'PrivateKey=[type]:[key]: %s" % self)
self.private_key_type, self.private_key = value.split(':', 1) + elif key == 'ClientAuth': + if ':' not in value: + raise stem.ProtocolError("ADD_ONION ClientAuth lines should be of the form 'ClientAuth=[username]:[credential]: %s" % self) + + username, credential = value.split(':', 1) + self.client_auth[username] = credential diff --git a/stem/version.py b/stem/version.py index df89d28..2dbcff2 100644 --- a/stem/version.py +++ b/stem/version.py @@ -58,6 +58,7 @@ easily parsed and compared, for instance... **HSFETCH** HSFETCH requests **HSPOST** HSPOST requests **ADD_ONION** ADD_ONION and DEL_ONION requests + **ADD_ONION_BASIC_AUTH** ADD_ONION supports basic authentication **LOADCONF** LOADCONF requests **MICRODESCRIPTOR_IS_DEFAULT** Tor gets microdescriptors by default rather than server descriptors **TAKEOWNERSHIP** TAKEOWNERSHIP requests @@ -372,6 +373,7 @@ Requirement = stem.util.enum.Enum( ('HSFETCH', Version('0.2.7.1-alpha')), ('HSPOST', Version('0.2.7.1-alpha')), ('ADD_ONION', Version('0.2.7.1-alpha')), + ('ADD_ONION_BASIC_AUTH', Version('0.2.9.1-alpha')), ('LOADCONF', Version('0.2.1.1')), ('MICRODESCRIPTOR_IS_DEFAULT', Version('0.2.3.3')), ('TAKEOWNERSHIP', Version('0.2.2.28-beta')), diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py index d5c2ec1..2bd06a6 100644 --- a/test/integ/control/controller.py +++ b/test/integ/control/controller.py @@ -641,6 +641,7 @@ class TestController(unittest.TestCase): response = controller.create_ephemeral_hidden_service(4567) self.assertEqual([response.service_id], controller.list_ephemeral_hidden_services()) self.assertTrue(response.private_key is not None) + self.assertEqual({}, response.client_auth)
# drop the service
@@ -672,6 +673,26 @@ class TestController(unittest.TestCase): self.assertEqual(0, len(second_controller.list_ephemeral_hidden_services()))
@require_controller + @require_version(Requirement.ADD_ONION_BASIC_AUTH) + def test_with_ephemeral_hidden_services_with_basic_auth(self): + """ + Exercises creating ephemeral hidden services that uses basic authentication. + """ + + runner = test.runner.get_runner() + + with runner.get_tor_controller() as controller: + response = controller.create_ephemeral_hidden_service(4567, basic_auth = {'alice': 'nKwfvVPmTNr2k2pG0pzV4g', 'bob': None}) + self.assertEqual([response.service_id], controller.list_ephemeral_hidden_services()) + self.assertTrue(response.private_key is not None) + self.assertEqual(['bob'], response.client_auth.keys()) # newly created credentials were only created for bob + + # drop the service + + self.assertEqual(True, controller.remove_ephemeral_hidden_service(response.service_id)) + self.assertEqual([], controller.list_ephemeral_hidden_services()) + + @require_controller @require_version(Requirement.ADD_ONION) def test_with_detached_ephemeral_hidden_services(self): """ diff --git a/test/unit/doctest.py b/test/unit/doctest.py index e3e53d8..548831a 100644 --- a/test/unit/doctest.py +++ b/test/unit/doctest.py @@ -14,6 +14,7 @@ import stem.util.str_tools import stem.util.system import stem.version
+import test.mocking import test.util
try: @@ -27,6 +28,12 @@ EXPECTED_CIRCUIT_STATUS = """\ 19 BUILT $718BCEA286B531757ACAFF93AE04910EA73DE617=KsmoinOK,$30BAB8EE7606CBD12F3CC269AE976E0153E7A58D=Pascal1,$2765D8A8C4BBA3F89585A9FFE0E8575615880BEB=Anthracite PURPOSE=GENERAL TIME_CREATED=2012-12-06T13:50:56.969938\ """
+ADD_ONION_RESPONSE = """\ +250-ServiceID=oekn5sqrvcu4wote +250-ClientAuth=bob:nKwfvVPmTNr2k2pG0pzV4g +250 OK +""" +
class TestDocumentation(unittest.TestCase): def test_examples(self): @@ -77,6 +84,10 @@ class TestDocumentation(unittest.TestCase): 'circuit-status': EXPECTED_CIRCUIT_STATUS, }[arg]
+ response = test.mocking.get_message(ADD_ONION_RESPONSE) + stem.response.convert('ADD_ONION', response) + controller.create_ephemeral_hidden_service.return_value = response + args['globs'] = {'controller': controller} test_run = doctest.testfile(path, **args) elif path.endswith('/stem/version.py'): diff --git a/test/unit/response/add_onion.py b/test/unit/response/add_onion.py index b58fe3b..64e3688 100644 --- a/test/unit/response/add_onion.py +++ b/test/unit/response/add_onion.py @@ -14,6 +14,12 @@ WITH_PRIVATE_KEY = """250-ServiceID=gfzprpioee3hoppz 250-PrivateKey=RSA1024:MIICXgIBAAKBgQDZvYVxvKPTWhId/8Ss9fVxjAoFDsrJ3pk6HjHrEFRm3ypkK/vArbG9BrupzzYcyms+lO06O8b/iOSHuZI5mUEGkrYqQ+hpB2SkPUEzW7vcp8SQQivna3+LfkWH4JDqfiwZutU6MMEvU6g1OqK4Hll6uHbLpsfxkS/mGjyu1C9a9wIDAQABAoGBAJxsC3a25xZJqaRFfxwmIiptSTFy+/nj4T4gPQo6k/fHMKP/+P7liT9bm+uUwbITNNIjmPzxvrcKt+pNRR/92fizxr8QXr8l0ciVOLerbvdqvVUaQ/K1IVsblOLbactMvXcHactmqqLFUaZU9PPSDla7YkzikLDIUtHXQBEt4HEhAkEA/c4n+kpwi4odCaF49ESPbZC/Qejh7U9Tq10vAHzfrrGgQjnLw2UGDxJQXc9P12fGTvD2q3Q3VaMI8TKKFqZXsQJBANufh1zfP+xX/UfxJ4QzDUCHCu2gnyTDj3nG9Bc80E5g7NwR2VBXF1R+QQCK9GZcXd2y6vBYgrHOSUiLbVjGrycCQQDpOcs0zbjUEUuTsQUT+fiO50dJSrZpus6ZFxz85sMppeItWSzsVeYWbW7adYnZ2Gu72OPjM/0xPYsXEakhHSRRAkAxlVauNQjthv/72god4pi/VL224GiNmEkwKSa6iFRPHbrcBHuXk9IElWx/ft+mrHvUraw1DwaStgv9gNzzCghJAkEA08RegCRnIzuGvgeejLk4suIeCMD/11AvmSvxbRWS5rq1leSVo7uGLSnqDbwlzE4dGb5kH15NNAp14/l2Fu/yZg== 250 OK"""
+WITH_CLIENT_AUTH = """250-ServiceID=oekn5sqrvcu4wote +250-ClientAuth=bob:lhwLVFt0Kd5/0Gy9DkKoyA +250-ClientAuth=alice:T9UADxtrvqx2HnLKWp/fWQ +250 OK +""" + WITHOUT_PRIVATE_KEY = """250-ServiceID=gfzprpioee3hoppz 250 OK"""
@@ -57,6 +63,20 @@ class TestAddOnionResponse(unittest.TestCase): self.assertEqual('gfzprpioee3hoppz', response.service_id) self.assertTrue(response.private_key.startswith('MIICXgIBAAKB')) self.assertEqual('RSA1024', response.private_key_type) + self.assertEqual({}, response.client_auth) + + def test_with_client_auth(self): + """ + Checks a response when there's client credentials. + """ + + response = mocking.get_message(WITH_CLIENT_AUTH) + stem.response.convert('ADD_ONION', response) + + self.assertEqual('oekn5sqrvcu4wote', response.service_id) + self.assertEqual(None, response.private_key) + self.assertEqual(None, response.private_key_type) + self.assertEqual({'bob': 'lhwLVFt0Kd5/0Gy9DkKoyA', 'alice': 'T9UADxtrvqx2HnLKWp/fWQ'}, response.client_auth)
def test_without_private_key(self): """
tor-commits@lists.torproject.org