[tor-commits] [stem/master] Basic auth support for ADD_ONION

atagar at torproject.org atagar at torproject.org
Thu Jun 16 17:02:47 UTC 2016


commit 38aa76c0216e083b4eb5eb6cfc1deba03cf44988
Author: Damian Johnson <atagar at 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):
     """



More information about the tor-commits mailing list