commit b86a709cdba4c57f5e5fa9418f6e795dcfa8c63b Author: Damian Johnson atagar@torproject.org Date: Sat Sep 14 13:15:57 2013 -0700
Helper function for authenticating to the tor controller
More refactoring for arm's starter module. This moves authentication into a helper method, greatly expanding the error output for users and adds unit tests. --- arm/settings.cfg | 28 +++++++++++++++++++ arm/starter.py | 79 +++++++++++++++++++++++++++++++++++++----------------- test/starter.py | 74 +++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 26 deletions(-)
diff --git a/arm/settings.cfg b/arm/settings.cfg index c73a16f..368d418 100644 --- a/arm/settings.cfg +++ b/arm/settings.cfg @@ -18,6 +18,34 @@ msg.help |arm -b -i 1643 hide connection data, attaching to control port 1643 |arm -e we -c /tmp/cfg use this configuration file with 'WARN'/'ERR' events
+msg.wrong_port_type +|Please check in your torrc that %i is the ControlPort. Maybe you configured +|it to be the ORPort or SocksPort instead? + +msg.wrong_socket_type +|Unable to connect to tor. Are you sure the interface you specified belongs to +|tor? + +msg.uncrcognized_auth_type +|Tor is using a type of authentication we do not recognize... +| +| %s +| +|Please check that arm is up to date and if there is an existing issue on +|'http://bugs.torproject.org'. If there isn't one then let us know! + +msg.missing_password_bug +|BUG: You provided a password but despite this stem reported that it was +|missing. This shouldn't happen - please let us know about it! +| +| http://bugs.torproject.org + +msg.unreadable_cookie_file +|We were unable to read tor's authentication cookie... +| +| Path: %s +| Issue: %s + # Important tor configuration options (shown by default) config.important BandwidthRate config.important BandwidthBurst diff --git a/arm/starter.py b/arm/starter.py index 057fb86..fa2289d 100644 --- a/arm/starter.py +++ b/arm/starter.py @@ -8,13 +8,13 @@ command line parameters.
import collections import getopt +import getpass import os import sys
import stem.util.connection
import time -import getpass import locale import logging import platform @@ -42,6 +42,11 @@ CONFIG = stem.util.conf.config_dict("arm", { "startup.blindModeEnabled": False, "startup.events": "N3", "msg.help": "", + "msg.wrong_port_type": "", + "msg.wrong_socket_type": "", + "msg.uncrcognized_auth_type": "", + "msg.missing_password_bug": "", + "msg.unreadable_cookie_file": "", })
# notices given if the user is running arm or tor as root @@ -80,6 +85,16 @@ ARGS = { OPT = "gi:s:c:dbe:vh" OPT_EXPANDED = ["interface=", "socket=", "config=", "debug", "blind", "event=", "version", "help"]
+try: + pathPrefix = os.path.dirname(sys.argv[0]) + if pathPrefix and not pathPrefix.endswith("/"): + pathPrefix = pathPrefix + "/" + + config = stem.util.conf.get_config("arm") + config.load("%sarm/settings.cfg" % pathPrefix) +except IOError, exc: + stem.util.log.warn(NO_INTERNAL_CFG_MSG % arm.util.sysTools.getFileErrorMsg(exc)) +
def _get_args(argv): """ @@ -168,6 +183,43 @@ def _get_controller(args): raise ValueError("Unable to connect to tor. Maybe it's running without a ControlPort?")
+def _authenticate(controller, password): + """ + Authenticates to the given Controller. + + :param stem.control.Controller controller: controller to be authenticated to + :param str args: password to authenticate with, **None** if nothing was provided + + :raises: **ValueError** if unable to authenticate + """ + + chroot = arm.util.torTools.get_chroot() + + try: + controller.authenticate(password = password, chroot_path = chroot) + except stem.connection.IncorrectSocketType: + control_socket = controller.get_socket() + + if isinstance(control_socket, stem.socket.ControlPort): + raise ValueError(CONFIG['msg.wrong_port_type'] % control_socket.get_port()) + else: + raise ValueError(CONFIG['msg.wrong_socket_type']) + except stem.connection.UnrecognizedAuthMethods as exc: + raise ValueError(CONFIG['msg.uncrcognized_auth_type'] % ', '.join(exc.unknown_auth_methods)) + except stem.connection.IncorrectPassword: + raise ValueError("Incorrect password") + except stem.connection.MissingPassword: + if password: + raise ValueError(CONFIG['msg.missing_password_bug']) + + password = getpass.getpass("Tor controller password: ") + return _authenticate(controller, password) + except stem.connection.UnreadableCookieFile as exc: + raise ValueError(CONFIG['msg.unreadable_cookie_file'] % (exc.cookie_path, str(exc))) + except stem.connection.AuthenticationFailure as exc: + raise ValueError("Unable to authenticate: %s" % exc) + + def _dumpConfig(): """ Dumps the current arm and tor configurations at the DEBUG runlevel. This @@ -222,11 +274,6 @@ def main(): pathPrefix = pathPrefix + "/"
try: - config.load("%sarm/settings.cfg" % pathPrefix) - except IOError, exc: - stem.util.log.warn(NO_INTERNAL_CFG_MSG % arm.util.sysTools.getFileErrorMsg(exc)) - - try: args = _get_args(sys.argv[1:]) except getopt.GetoptError as exc: print "%s (for usage provide --help)" % exc @@ -289,29 +336,11 @@ def main():
try: controller = _get_controller(args) + _authenticate(controller, CONFIG['tor.password']) except ValueError as exc: print exc exit(1)
- chroot = arm.util.torTools.get_chroot() - - try: - controller.authenticate(password = CONFIG["tor.password"], chroot_path = chroot) - except (stem.connection.MissingPassword, stem.connection.IncorrectPassword) as exc: - if isinstance(stem.connection.IncorrectPassword, exc): - print "Password found in '%s' was incorrect" % args.config - - try: - passphrase = getpass.getpass("Controller password: ") - controller.authenticate(password = passphrase, chroot_path = chroot) - del passphrase # removing reference as early as possible to free memory - except stem.connection.IncorrectPassword: - print "Incorrect password" - sys.exit(1) - except stem.connection.AuthenticationFailure as exc: - print "Unable to authenticate: %s" % exc - sys.exit(1) - # Removing references to the controller password so the memory can be # freed. Without direct memory access this is about the best we can do.
diff --git a/test/starter.py b/test/starter.py index f8c4f0c..d1c90cf 100644 --- a/test/starter.py +++ b/test/starter.py @@ -7,9 +7,16 @@ import unittest
from mock import Mock, patch
-from arm.starter import _get_args, _get_controller, ARGS +from arm.starter import ( + _get_args, + _get_controller, + _authenticate, + ARGS, +)
import stem +import stem.connection +import stem.socket
class TestArgumentParsing(unittest.TestCase): def test_that_we_get_default_values(self): @@ -127,3 +134,68 @@ class TestGetController(unittest.TestCase): self.fail() except ValueError, exc: self.assertEqual(msg, str(exc)) + +class TestAuthenticate(unittest.TestCase): + @patch('arm.util.torTools.get_chroot') + def test_success(self, get_chroot_mock): + controller = Mock() + + get_chroot_mock.return_value = '' # no chroot + _authenticate(controller, None) + controller.authenticate.assert_called_with(password = None, chroot_path = '') + controller.authenticate.reset_mock() + + get_chroot_mock.return_value = '/my/chroot' + _authenticate(controller, 's3krit!!!') + controller.authenticate.assert_called_with(password = 's3krit!!!', chroot_path = '/my/chroot') + + @patch('arm.util.torTools.get_chroot', Mock(return_value = '')) + @patch('getpass.getpass') + def test_success_with_password_prompt(self, getpass_mock): + controller = Mock() + + def authenticate_mock(password, **kwargs): + if password is None: + raise stem.connection.MissingPassword('no password') + elif password == 'my_password': + return None # success + else: + raise ValueError("Unexpected authenticate_mock input: %s" % password) + + controller.authenticate.side_effect = authenticate_mock + getpass_mock.return_value = 'my_password' + + _authenticate(controller, None) + controller.authenticate.assert_any_call(password = None, chroot_path = '') + controller.authenticate.assert_any_call(password = 'my_password', chroot_path = '') + + @patch('arm.util.torTools.get_chroot', Mock(return_value = '')) + def test_failure(self): + controller = Mock() + + controller.authenticate.side_effect = stem.connection.IncorrectSocketType('unable to connect to socket') + controller.get_socket.return_value = stem.socket.ControlPort(connect = False) + self._assert_authenticate_fails_with(controller, 'Please check in your torrc that 9051 is the ControlPort.') + + controller.get_socket.return_value = stem.socket.ControlSocketFile(connect = False) + self._assert_authenticate_fails_with(controller, 'Are you sure the interface you specified belongs to') + + controller.authenticate.side_effect = stem.connection.UnrecognizedAuthMethods('unable to connect', ['telepathy']) + self._assert_authenticate_fails_with(controller, 'Tor is using a type of authentication we do not recognize...\n\n telepathy') + + controller.authenticate.side_effect = stem.connection.IncorrectPassword('password rejected') + self._assert_authenticate_fails_with(controller, 'Incorrect password') + + controller.authenticate.side_effect = stem.connection.UnreadableCookieFile('permission denied', '/tmp/my_cookie', False) + self._assert_authenticate_fails_with(controller, "We were unable to read tor's authentication cookie...\n\n Path: /tmp/my_cookie\n Issue: permission denied") + + controller.authenticate.side_effect = stem.connection.OpenAuthRejected('crazy failure') + self._assert_authenticate_fails_with(controller, 'Unable to authenticate: crazy failure') + + def _assert_authenticate_fails_with(self, controller, msg): + try: + _get_controller(_authenticate(controller, None)) + self.fail() + except ValueError, exc: + if not msg in str(exc): + self.fail("Expected...\n\n%s\n\n... which couldn't be found in...\n\n%s" % (msg, exc))
tor-commits@lists.torproject.org