[tor-commits] [stem/master] Handling chroot in stem.connection and integ tests

atagar at torproject.org atagar at torproject.org
Sun Apr 22 22:42:59 UTC 2012


commit 636fd3704d6ddcba6a2a780119570fc2e25dd77f
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Apr 22 15:05:17 2012 -0700

    Handling chroot in stem.connection and integ tests
    
    On ticket 4896 [1] I proposed a couple ideas for how we could handle chroot
    setups, but on reflection neither of them were very good.
    
    At a low level we can't reliably expand paths, nor should we try. If the user
    makes a raw 'GETINFO config-file' query then we should simply return what tor
    gives us, not try to 'fix' it by expanding the path.
    
    Rather, we should correct for chroot prefixes at a higher level like the
    controller. My current plan is...
    
    * The Controller class will have an optional chroot_path constructor argument,
      and a get_chroot() method. All of the Controller's methods and those of
      subclasses should take it into account for tor resource paths.
    
    * The stem.connection functions now accept a chroot_path argument. We need this
      since they will construct Controller instances, and also do the initial
      authentication (we need to know about chroots for cookie authentication).
    
    [1] https://trac.torproject.org/projects/tor/ticket/4896
---
 stem/connection.py                      |   25 +++++++++++-----
 test/integ/connection/authentication.py |   16 ++++++-----
 test/integ/connection/connect.py        |    5 +++-
 test/integ/connection/protocolinfo.py   |    9 +++++-
 test/integ/control/base_controller.py   |    6 ++--
 test/integ/socket/control_message.py    |    4 +++
 test/runner.py                          |   45 +++++++++++++++++++++++-------
 test/unit/connection/authentication.py  |    4 +-
 8 files changed, 80 insertions(+), 34 deletions(-)

diff --git a/stem/connection.py b/stem/connection.py
index 419dfa3..d950049 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -194,7 +194,7 @@ AUTHENTICATE_EXCEPTIONS = (
   AuthenticationFailure,
 )
 
-def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = None, controller = Controller.NONE):
+def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = None, chroot_path = None, controller = Controller.NONE):
   """
   Convenience function for quickly getting a control connection. This is very
   handy for debugging or CLI setup, handling setup and prompting for a password
@@ -205,6 +205,7 @@ def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = Non
     control_addr (str)      - ip address of the controller
     control_port (int)      - port number of the controller
     password (str)          - passphrase to authenticate to the socket
+    chroot_path (str)       - path prefix if in a chroot environment
     controller (Controller) - controller type to be returned
   
   Returns:
@@ -220,9 +221,9 @@ def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = Non
     print exc
     return None
   
-  return _connect(control_port, password, controller)
+  return _connect(control_port, password, chroot_path, controller)
 
-def connect_socket_file(socket_path = "/var/run/tor/control", password = None, controller = Controller.NONE):
+def connect_socket_file(socket_path = "/var/run/tor/control", password = None, chroot_path = None, controller = Controller.NONE):
   """
   Convenience function for quickly getting a control connection. For more
   information see the connect_port function.
@@ -230,6 +231,7 @@ def connect_socket_file(socket_path = "/var/run/tor/control", password = None, c
   Arguments:
     socket_path (str)       - path where the control socket is located
     password (str)          - passphrase to authenticate to the socket
+    chroot_path (str)       - path prefix if in a chroot environment
     controller (Controller) - controller type to be returned
   
   Returns:
@@ -242,15 +244,16 @@ def connect_socket_file(socket_path = "/var/run/tor/control", password = None, c
     print exc
     return None
   
-  return _connect(control_socket, password, controller)
+  return _connect(control_socket, password, chroot_path, controller)
 
-def _connect(control_socket, password, controller):
+def _connect(control_socket, password, chroot_path, controller):
   """
   Common implementation for the connect_* functions.
   
   Arguments:
     control_socket (stem.socket.ControlSocket) - socket being authenticated to
     password (str)          - passphrase to authenticate to the socket
+    chroot_path (str)       - path prefix if in a chroot environment
     controller (Controller) - controller type to be returned
   
   Returns:
@@ -258,7 +261,7 @@ def _connect(control_socket, password, controller):
   """
   
   try:
-    authenticate(control_socket, password)
+    authenticate(control_socket, password, chroot_path)
     
     if controller == Controller.NONE:
       return control_socket
@@ -274,7 +277,7 @@ def _connect(control_socket, password, controller):
     print "Unable to authenticate: %s" % exc
     return None
 
-def authenticate(control_socket, password = None, protocolinfo_response = None):
+def authenticate(control_socket, password = None, chroot_path = None, protocolinfo_response = None):
   """
   Authenticates to a control socket using the information provided by a
   PROTOCOLINFO response. In practice this will often be all we need to
@@ -288,6 +291,7 @@ def authenticate(control_socket, password = None, protocolinfo_response = None):
     control_socket (stem.socket.ControlSocket) - socket to be authenticated
     password (str) - passphrase to present to the socket if it uses password
         authentication (skips password auth if None)
+    chroot_path (str) - path prefix if in a chroot environment
     protocolinfo_response (stem.connection.ProtocolInfoResponse) -
         tor protocolinfo response, this is retrieved on our own if None
   
@@ -397,7 +401,12 @@ def authenticate(control_socket, password = None, protocolinfo_response = None):
       elif auth_type == AuthMethod.PASSWORD:
         authenticate_password(control_socket, password, False)
       elif auth_type == AuthMethod.COOKIE:
-        authenticate_cookie(control_socket, protocolinfo_response.cookie_path, False)
+        cookie_path = protocolinfo_response.cookie_path
+        
+        if chroot_path:
+          cookie_path = os.path.join(chroot_path, cookie_path.lstrip(os.path.sep))
+        
+        authenticate_cookie(control_socket, cookie_path, False)
       
       return # success!
     except OpenAuthRejected, exc:
diff --git a/test/integ/connection/authentication.py b/test/integ/connection/authentication.py
index aafde5d..f80b848 100644
--- a/test/integ/connection/authentication.py
+++ b/test/integ/connection/authentication.py
@@ -85,8 +85,9 @@ class TestAuthenticate(unittest.TestCase):
     Tests that the authenticate function can authenticate to our socket.
     """
     
-    with test.runner.get_runner().get_tor_socket(False) as control_socket:
-      stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD)
+    runner = test.runner.get_runner()
+    with runner.get_tor_socket(False) as control_socket:
+      stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD, runner.get_chroot())
       test.runner.exercise_socket(self, control_socket)
   
   def test_authenticate_general_example(self):
@@ -94,7 +95,8 @@ class TestAuthenticate(unittest.TestCase):
     Tests the authenticate function with something like its pydoc example.
     """
     
-    tor_options = test.runner.get_runner().get_options()
+    runner = test.runner.get_runner()
+    tor_options = runner.get_options()
     
     try:
       control_socket = stem.socket.ControlPort(control_port = test.runner.CONTROL_PORT)
@@ -105,7 +107,7 @@ class TestAuthenticate(unittest.TestCase):
     
     try:
       # this authenticate call should work for everything but password-only auth
-      stem.connection.authenticate(control_socket)
+      stem.connection.authenticate(control_socket, chroot_path = runner.get_chroot())
       test.runner.exercise_socket(self, control_socket)
     except stem.connection.IncorrectSocketType:
       self.fail()
@@ -141,7 +143,7 @@ class TestAuthenticate(unittest.TestCase):
       if is_password_only:
         self.assertRaises(stem.connection.MissingPassword, stem.connection.authenticate, control_socket)
       else:
-        stem.connection.authenticate(control_socket)
+        stem.connection.authenticate(control_socket, chroot_path = runner.get_chroot())
         test.runner.exercise_socket(self, control_socket)
     
     # tests with the incorrect password
@@ -149,12 +151,12 @@ class TestAuthenticate(unittest.TestCase):
       if is_password_only:
         self.assertRaises(stem.connection.IncorrectPassword, stem.connection.authenticate, control_socket, "blarg")
       else:
-        stem.connection.authenticate(control_socket, "blarg")
+        stem.connection.authenticate(control_socket, "blarg", runner.get_chroot())
         test.runner.exercise_socket(self, control_socket)
     
     # tests with the right password
     with runner.get_tor_socket(False) as control_socket:
-      stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD)
+      stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD, runner.get_chroot())
       test.runner.exercise_socket(self, control_socket)
   
   def test_authenticate_none(self):
diff --git a/test/integ/connection/connect.py b/test/integ/connection/connect.py
index 9b6337a..d61ddf7 100644
--- a/test/integ/connection/connect.py
+++ b/test/integ/connection/connect.py
@@ -25,12 +25,15 @@ class TestConnect(unittest.TestCase):
     Basic sanity checks for the connect_port function.
     """
     
+    runner = test.runner.get_runner()
+    
     control_socket = stem.connection.connect_port(
       control_port = test.runner.CONTROL_PORT,
       password = test.runner.CONTROL_PASSWORD,
+      chroot_path = runner.get_chroot(),
       controller = stem.connection.Controller.NONE)
     
-    if test.runner.Torrc.PORT in test.runner.get_runner().get_options():
+    if test.runner.Torrc.PORT in runner.get_options():
       test.runner.exercise_socket(self, control_socket)
       control_socket.close()
     else:
diff --git a/test/integ/connection/protocolinfo.py b/test/integ/connection/protocolinfo.py
index a0214a2..728c746 100644
--- a/test/integ/connection/protocolinfo.py
+++ b/test/integ/connection/protocolinfo.py
@@ -114,12 +114,17 @@ class TestProtocolInfo(unittest.TestCase):
     the test configuration.
     """
     
-    tor_options = test.runner.get_runner().get_options()
+    runner = test.runner.get_runner()
+    tor_options = runner.get_options()
     auth_methods, auth_cookie_path = [], None
     
     if test.runner.Torrc.COOKIE in tor_options:
       auth_methods.append(stem.connection.AuthMethod.COOKIE)
-      auth_cookie_path = test.runner.get_runner().get_auth_cookie_path()
+      chroot_path = runner.get_chroot()
+      auth_cookie_path = runner.get_auth_cookie_path()
+      
+      if chroot_path and auth_cookie_path.startswith(chroot_path):
+        auth_cookie_path = auth_cookie_path[len(chroot_path):]
     
     if test.runner.Torrc.PASSWORD in tor_options:
       auth_methods.append(stem.connection.AuthMethod.PASSWORD)
diff --git a/test/integ/control/base_controller.py b/test/integ/control/base_controller.py
index da4cc08..1abab93 100644
--- a/test/integ/control/base_controller.py
+++ b/test/integ/control/base_controller.py
@@ -77,10 +77,10 @@ class TestBaseController(unittest.TestCase):
     runner = test.runner.get_runner()
     with runner.get_tor_socket() as control_socket:
       controller = stem.control.BaseController(control_socket)
-      response = controller.msg("GETINFO config-file")
+      response = controller.msg("GETINFO version")
       
-      torrc_dst = runner.get_torrc_path()
-      self.assertEquals("config-file=%s\nOK" % torrc_dst, str(response))
+      tor_version = runner.get_tor_version()
+      self.assertEquals("version=%s\nOK" % tor_version, str(response))
   
   def test_msg_invalid(self):
     """
diff --git a/test/integ/socket/control_message.py b/test/integ/socket/control_message.py
index 72f9174..255e66d 100644
--- a/test/integ/socket/control_message.py
+++ b/test/integ/socket/control_message.py
@@ -86,6 +86,10 @@ class TestControlMessage(unittest.TestCase):
     
     runner = test.runner.get_runner()
     torrc_dst = runner.get_torrc_path()
+    chroot_path = runner.get_chroot()
+    
+    if chroot_path and torrc_dst.startswith(chroot_path):
+      torrc_dst = torrc_dst[len(chroot_path):]
     
     with runner.get_tor_socket() as control_socket:
       control_socket.send("GETINFO config-file")
diff --git a/test/runner.py b/test/runner.py
index 3636b44..6702b8b 100644
--- a/test/runner.py
+++ b/test/runner.py
@@ -21,6 +21,9 @@ Runner - Runtime context for our integration tests.
   |- get_test_dir - testing directory path
   |- get_torrc_path - path to our tor instance's torrc
   |- get_torrc_contents - contents of our tor instance's torrc
+  |- get_auth_cookie_path - path for our authentication cookie if we have one
+  |- get_tor_cwd - current working directory of our tor process
+  |- get_chroot - provides the path of our emulated chroot if we have one
   |- get_pid - process id of our tor process
   |- get_tor_socket - provides a socket to the tor instance
   |- get_tor_version - provides the version of tor we're running against
@@ -122,7 +125,12 @@ def exercise_socket(test_case, control_socket):
     control_socket (stem.socket.ControlSocket) - socket to be tested
   """
   
-  torrc_path = get_runner().get_torrc_path()
+  runner = get_runner()
+  torrc_path, chroot_path = runner.get_torrc_path(), runner.get_chroot()
+  
+  if chroot_path and torrc_path.startswith(chroot_path):
+    torrc_path = torrc_path[len(chroot_path):]
+  
   control_socket.send("GETINFO config-file")
   config_file_response = control_socket.recv()
   test_case.assertEquals("config-file=%s\nOK" % torrc_path, str(config_file_response))
@@ -136,7 +144,7 @@ def get_runner():
   if not INTEG_RUNNER: INTEG_RUNNER = Runner()
   return INTEG_RUNNER
 
-class MockChrootFile:
+class _MockChrootFile:
   """
   Wrapper around a file object that strips given content from readline()
   responses. This is used to simulate a chroot setup by removing the restign
@@ -161,6 +169,7 @@ class Runner:
     self._torrc_contents = ""
     self._custom_opts = None
     self._tor_process = None
+    self._chroot_path = None
     
     # set if we monkey patch stem.socket.recv_message()
     
@@ -225,9 +234,10 @@ class Runner:
           # need to set that too
           
           self._original_recv_message = stem.socket.recv_message
+          self._chroot_path = data_dir_path
           
           def _chroot_recv_message(control_file):
-            return self._original_recv_message(MockChrootFile(control_file, data_dir_path))
+            return self._original_recv_message(_MockChrootFile(control_file, data_dir_path))
           
           stem.socket.recv_message = _chroot_recv_message
         
@@ -356,9 +366,24 @@ class Runner:
     test_dir = self._get("_test_dir")
     return os.path.join(test_dir, "torrc")
   
+  def get_torrc_contents(self):
+    """
+    Provides the contents of our torrc.
+    
+    Returns:
+      str with the contents of our torrc, lines are newline separated
+    
+    Raises:
+      RunnerStopped if we aren't running
+    """
+    
+    return self._get("_torrc_contents")
+  
   def get_auth_cookie_path(self):
     """
     Provides the absolute path for our authentication cookie if we have one.
+    If running with an emulated chroot this is uneffected, still providing the
+    real path.
     
     Returns:
       str with our auth cookie path
@@ -377,18 +402,16 @@ class Runner:
     
     return self._get("_tor_cwd")
   
-  def get_torrc_contents(self):
+  def get_chroot(self):
     """
-    Provides the contents of our torrc.
+    Provides the path we're using to emulate a chroot environment. This is None
+    if we aren't emulating a chroot setup.
     
     Returns:
-      str with the contents of our torrc, lines are newline separated
-    
-    Raises:
-      RunnerStopped if we aren't running
+      str with the path of our emulated chroot
     """
     
-    return self._get("_torrc_contents")
+    return self._chroot_path
   
   def get_pid(self):
     """
@@ -425,7 +448,7 @@ class Runner:
     else: raise TorInaccessable("Unable to connect to tor")
     
     if authenticate:
-      stem.connection.authenticate(control_socket, CONTROL_PASSWORD)
+      stem.connection.authenticate(control_socket, CONTROL_PASSWORD, self.get_chroot())
     
     return control_socket
   
diff --git a/test/unit/connection/authentication.py b/test/unit/connection/authentication.py
index 0937439..831ca62 100644
--- a/test/unit/connection/authentication.py
+++ b/test/unit/connection/authentication.py
@@ -140,9 +140,9 @@ class TestAuthenticate(unittest.TestCase):
                 mocking.mock(auth_function, mocking.raise_exception(raised_exc))
             
             if expect_success:
-              stem.connection.authenticate(None, "blah", protocolinfo_arg)
+              stem.connection.authenticate(None, "blah", None, protocolinfo_arg)
             else:
-              self.assertRaises(stem.connection.AuthenticationFailure, stem.connection.authenticate, None, "blah", protocolinfo_arg)
+              self.assertRaises(stem.connection.AuthenticationFailure, stem.connection.authenticate, None, "blah", None, protocolinfo_arg)
     
     # revert logging back to normal
     stem_logger.setLevel(log.logging_level(log.TRACE))



More information about the tor-commits mailing list