commit 636fd3704d6ddcba6a2a780119570fc2e25dd77f Author: Damian Johnson atagar@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))
tor-commits@lists.torproject.org