commit a9a240c553cc154f8019c6332ece686202398c34 Author: Damian Johnson atagar@torproject.org Date: Tue Dec 27 09:58:00 2011 -0800
Implementing and testing connect_* functions
Similiar to TorCtl.connect(), stem's connect_port and connect_socket_file are convenience functions for trivially getting an authenticated connection. This isn't ideal for applications since it hijacks stdin/stdout and lacks exceptions, however for CLI apps and the interactive interpretor it's very nice to have. --- run_tests.py | 2 + stem/connection.py | 87 +++++++++++++++++++++++++++++++ test/integ/connection/__init__.py | 2 +- test/integ/connection/authentication.py | 25 +++------ test/integ/connection/connect.py | 69 ++++++++++++++++++++++++ test/runner.py | 15 +++++ 6 files changed, 181 insertions(+), 19 deletions(-)
diff --git a/run_tests.py b/run_tests.py index 73cfb9c..7247dd7 100755 --- a/run_tests.py +++ b/run_tests.py @@ -24,6 +24,7 @@ import test.integ.socket.control_message import test.integ.util.conf import test.integ.util.system import test.integ.connection.authentication +import test.integ.connection.connect import test.integ.connection.protocolinfo
import stem.util.enum @@ -45,6 +46,7 @@ UNIT_TESTS = (("stem.socket.ControlMessage", test.unit.socket.control_message.Te
INTEG_TESTS = (("stem.socket.ControlMessage", test.integ.socket.control_message.TestControlMessage), ("stem.connection.authenticate", test.integ.connection.authentication.TestAuthenticate), + ("stem.connection.connect_*", test.integ.connection.connect.TestConnect), ("stem.connection.get_protocolinfo", test.integ.connection.protocolinfo.TestProtocolInfo), ("stem.util.conf", test.integ.util.conf.TestConf), ("stem.util.system", test.integ.util.system.TestSystem), diff --git a/stem/connection.py b/stem/connection.py index 3758187..25779f5 100644 --- a/stem/connection.py +++ b/stem/connection.py @@ -1,6 +1,9 @@ """ Functions for connecting and authenticating to the tor process.
+connect_port - Convenience method to get an authenticated control connection. +connect_socket_file - Similar to connect_port, but for control socket files. + authenticate - Main method for authenticating to a control socket. authenticate_none - Authenticates to an open control socket. authenticate_password - Authenticates to a socket supporting password auth. @@ -40,6 +43,7 @@ AuthenticationFailure - Base exception raised for authentication failures. """
import os +import getpass import logging import binascii
@@ -50,6 +54,9 @@ import stem.util.system
LOGGER = logging.getLogger("stem")
+# enums representing classes that the connect_* methods can return +Controller = stem.util.enum.Enum("NONE") + # Methods by which a controller can authenticate to the control port. Tor gives # a list of all the authentication methods it will accept in response to # PROTOCOLINFO queries. @@ -147,6 +154,86 @@ AUTHENTICATE_EXCEPTIONS = ( AuthenticationFailure, )
+def connect_port(control_addr = "127.0.0.1", control_port = 9051, password = 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 + if necessary (and none is provided). If any issues arise this prints a + description of the problem and returns None. + + Arguments: + control_addr (str) - ip address of the controller + control_port (int) - port number of the controller + password (str) - passphrase to authenticate to the socket + controller (Controller) - controller type to be returned + + Returns: + Authenticated control connection, the type based on the controller enum... + Controller.NONE => stem.socket.ControlPort + """ + + # TODO: replace the controller arg's default when we have something better + + try: + control_port = stem.socket.ControlPort(control_addr, control_port) + except stem.socket.SocketError, exc: + print exc + return None + + return _connect(control_port, password, controller) + +def connect_socket_file(socket_path = "/var/run/tor/control", password = None, controller = Controller.NONE): + """ + Convenience function for quickly getting a control connection. For more + information see the connect_port function. + + Arguments: + socket_path (str) - path where the control socket is located + password (str) - passphrase to authenticate to the socket + controller (Controller) - controller type to be returned + + Returns: + Authenticated control connection, the type based on the controller enum. + """ + + try: + control_socket = stem.socket.ControlSocketFile(socket_path) + except stem.socket.SocketError, exc: + print exc + return None + + return _connect(control_socket, password, controller) + +def _connect(control_socket, password, 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 + controller (Controller) - controller type to be returned + + Returns: + Authenticated control connection with a type based on the controller enum. + """ + + try: + authenticate(control_socket, password) + + if controller == Controller.NONE: + return control_socket + except MissingPassword: + assert password == None, "BUG: authenticate raised MissingPassword despite getting one" + + try: password = getpass.getpass("Controller password: ") + except KeyboardInterrupt: return None + + return _connect(control_socket, password, controller) + except AuthenticationFailure, exc: + control_socket.close() + print "Unable to authenticate: %s" % exc + return None + def authenticate(control_socket, password = None, protocolinfo_response = None): """ Authenticates to a control socket using the information provided by a diff --git a/test/integ/connection/__init__.py b/test/integ/connection/__init__.py index d570669..90471c0 100644 --- a/test/integ/connection/__init__.py +++ b/test/integ/connection/__init__.py @@ -2,5 +2,5 @@ Integration tests for stem.connection. """
-__all__ = ["authenticate", "protocolinfo"] +__all__ = ["authenticate", "connect", "protocolinfo"]
diff --git a/test/integ/connection/authentication.py b/test/integ/connection/authentication.py index a79cbb0..7ac745b 100644 --- a/test/integ/connection/authentication.py +++ b/test/integ/connection/authentication.py @@ -43,7 +43,7 @@ class TestAuthenticate(unittest.TestCase):
control_socket = test.runner.get_runner().get_tor_socket(False) stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD) - self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket) control_socket.close()
def test_authenticate_general_example(self): @@ -64,7 +64,7 @@ class TestAuthenticate(unittest.TestCase): try: # this authenticate call should work for everything but password-only auth stem.connection.authenticate(control_socket) - self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket) except stem.connection.IncorrectSocketType: self.fail() except stem.connection.MissingPassword: @@ -73,7 +73,7 @@ class TestAuthenticate(unittest.TestCase):
try: stem.connection.authenticate_password(control_socket, controller_password) - self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket) except stem.connection.PasswordAuthFailed: self.fail() except stem.connection.AuthenticationFailure: @@ -101,7 +101,7 @@ class TestAuthenticate(unittest.TestCase): self.assertRaises(stem.connection.MissingPassword, auth_function) else: auth_function() - self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket)
control_socket.close()
@@ -113,14 +113,14 @@ class TestAuthenticate(unittest.TestCase): self.assertRaises(stem.connection.IncorrectPassword, auth_function) else: auth_function() - self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket)
control_socket.close()
# tests with the right password control_socket = runner.get_tor_socket(False) stem.connection.authenticate(control_socket, test.runner.CONTROL_PASSWORD) - self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket) control_socket.close()
def test_authenticate_none(self): @@ -364,17 +364,6 @@ class TestAuthenticate(unittest.TestCase): control_socket.close() raise exc
- self._exercise_socket(control_socket) + test.runner.exercise_socket(self, control_socket) control_socket.close() - - def _exercise_socket(self, control_socket): - """ - Checks that we can now use the socket by issuing a 'GETINFO config-file' - query. - """ - - torrc_path = test.runner.get_runner().get_torrc_path() - control_socket.send("GETINFO config-file") - config_file_response = control_socket.recv() - self.assertEquals("config-file=%s\nOK" % torrc_path, str(config_file_response))
diff --git a/test/integ/connection/connect.py b/test/integ/connection/connect.py new file mode 100644 index 0000000..6b0c16e --- /dev/null +++ b/test/integ/connection/connect.py @@ -0,0 +1,69 @@ +""" +Integration tests for the connect_* convenience functions. +""" + +import sys +import unittest +import StringIO + +import stem.connection +import test.runner + +class TestConnect(unittest.TestCase): + """ + Tests the connection methods. This should be run with the 'CONN_ALL' integ + target to exercise the widest range of use cases. + """ + + def setUp(self): + connection_type = test.runner.get_runner().get_connection_type() + + # none of these tests apply if there's no control connection + if connection_type == test.runner.TorConnection.NONE: + self.skipTest("(no connection)") + + def test_connect_port(self): + """ + Basic sanity checks for the connect_port function. + """ + + self._test_connect(True) + + def test_connect_socket_file(self): + """ + Basic sanity checks for the connect_socket_file function. + """ + + self._test_connect(False) + + def _test_connect(self, is_port): + """ + Common implementations for the test_connect_* functions. + """ + + # prevents the function from printing to the real stdout + original_stdout = sys.stdout + sys.stdout = StringIO.StringIO() + + try: + connection_type = test.runner.get_runner().get_connection_type() + ctl_pw = test.runner.CONTROL_PASSWORD + controller = stem.connection.Controller.NONE + + if is_port: + opt_type = test.runner.OPT_PORT + ctl_port = test.runner.CONTROL_PORT + control_socket = stem.connection.connect_port(control_port = ctl_port, password = ctl_pw, controller = controller) + else: + opt_type = test.runner.OPT_SOCKET + ctl_socket = test.runner.CONTROL_SOCKET_PATH + control_socket = stem.connection.connect_socket_file(socket_path = ctl_socket, password = ctl_pw, controller = controller) + + if opt_type in test.runner.CONNECTION_OPTS[connection_type]: + test.runner.exercise_socket(self, control_socket) + control_socket.close() + else: + self.assertEquals(control_socket, None) + finally: + sys.stdout = original_stdout + diff --git a/test/runner.py b/test/runner.py index 6b5f6ea..ed92126 100644 --- a/test/runner.py +++ b/test/runner.py @@ -106,6 +106,21 @@ def get_torrc(connection_type = DEFAULT_TOR_CONNECTION): return torrc + "\n".join(connection_opt) + "\n" else: return torrc
+def exercise_socket(test_case, control_socket): + """ + Checks that we can now use the socket by issuing a 'GETINFO config-file' + query. + + Arguments: + test_case (unittest.TestCase) - unit testing case being ran + control_socket (stem.socket.ControlSocket) - socket to be tested + """ + + torrc_path = get_runner().get_torrc_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)) + class RunnerStopped(Exception): "Raised when we try to use a Runner that doesn't have an active tor instance" pass