commit a9a240c553cc154f8019c6332ece686202398c34
Author: Damian Johnson <atagar(a)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