[tor-commits] [stem/master] Better connect() function for easily getting a Controller

atagar at torproject.org atagar at torproject.org
Thu Apr 10 16:12:26 UTC 2014


commit 69433bdacc27d3db968b8fc414f1ee598ad54013
Author: Damian Johnson <atagar at torproject.org>
Date:   Sat Apr 5 15:48:48 2014 -0700

    Better connect() function for easily getting a Controller
    
    Stem provided a couple high level functions for getting a Controller with
    minimal fuss: connect_port() and connect_socket_file(). These interacted
    directly with stdin and stdout making them undesirable for sophisticated use
    cases, but for commandline scripts provided a one-line method of geting an
    authenticated Controller.
    
    Personally I never used these myself. Not because I didn't need this, but
    because I had done better in arm. In arm's connection helper it...
    
    * Provided better, more useful error output upon authentication failure.
    
    * Attempted to connect to both a control port and socket, then was smart about
      the message it presented if both failed.
    
    On reflection the first belongs in Stem (hell, why not provide better error
    output?!?). As for the second I realized that having separate control_port()
    and control_sockeet_file() functions was a mistake.
    
    This deprecates the old connect_* functions in favor of a single new connect()
    that does essentially exactly what we do in arm: try to get a tor connection,
    and give nice advice if we can't.
---
 docs/change_log.rst              |    1 +
 docs/faq.rst                     |    7 +-
 stem/connection.py               |  141 ++++++++++++++++++++++++++++++++++----
 stem/socket.py                   |    2 +-
 test/integ/connection/connect.py |   19 +++++
 5 files changed, 153 insertions(+), 17 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index a02750c..624f730 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -50,6 +50,7 @@ The following are only available within Stem's `git repository
   * Added `support for TB_EMPTY events <api/response.html#stem.response.events.TokenBucketEmptyEvent>`_ (:spec:`6f2919a`)
   * Added `support for HS_DESC events <api/response.html#stem.response.events.HSDescEvent>`_ (:spec:`a67ac4d`, :trac:`10807`)
   * Changed :func:`~stem.control.Controller.get_network_status` and :func:`~stem.control.Controller.get_network_statuses` to provide :class:`~stem.descriptor.router_status_entry.RouterStatusEntryMicroV3` if Tor is using microdescriptors (:trac:`7646`)
+  * Deprecated :func:`~stem.connection.connect_port` and :func:`~stem.connection.connect_socket_file` in favor of a new, better :func:`~stem.connection.connect` function
 
  * **Utilities**
 
diff --git a/docs/faq.rst b/docs/faq.rst
index 51aa527..a30601e 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -273,12 +273,11 @@ Once you have Tor running and `properly configured <tutorials/the_little_relay_t
 
 #. `Connection Module <api/connection.html>`_
 
-   Writing a commandline script? Then the :func:`~stem.connection.connect_port`
-   and :func:`~stem.connection.connect_socket_file` functions provide you the
-   quickest and most hassle free method for getting a
+   Writing a commandline script? Then the :func:`~stem.connection.connect`
+   function provide you the quickest and most hassle free method for getting a
    :class:`~stem.control.Controller`.
 
-   These functions connect and authenticate to the given port or socket,
+   This function connects and authenticates to the given port or socket,
    providing you a one-line method of getting a
    :class:`~stem.control.Controller` that's ready to use. If Tor requires a
    password then the user will be prompted for it. When the connection cannot
diff --git a/stem/connection.py b/stem/connection.py
index 769f432..1304357 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -4,8 +4,7 @@
 """
 Functions for connecting and authenticating to the tor process.
 
-The :func:`~stem.connection.connect_port` and
-:func:`~stem.connection.connect_socket_file` functions give an easy, one line
+The :func:`~stem.connection.connect` function give an easy, one line
 method for getting an authenticated control connection. This is handy for CLI
 applications and the python interactive interpreter, but does several things
 that makes it undesirable for applications (uses stdin/stdout, suppresses
@@ -13,12 +12,12 @@ exceptions, etc).
 
 ::
 
-  import sys 
+  import sys
 
-  from stem.connection import connect_port
+  from stem.connection import connect
 
   if __name__ == '__main__':
-    controller = connect_port()
+    controller = connect()
 
     if not controller:
       sys.exit(1)  # unable to get a connection
@@ -28,14 +27,14 @@ exceptions, etc).
 
 ::
 
-  % python example.py 
+  % python example.py
   Tor is running version 0.2.4.10-alpha-dev (git-8be6058d8f31e578)
 
 ... or if Tor isn't running...
 
 ::
 
-  % python example.py 
+  % python example.py
   [Errno 111] Connection refused
 
 The :func:`~stem.connection.authenticate` function, however, gives easy but
@@ -76,8 +75,7 @@ fine-grained control over the authentication process. For instance...
 
 ::
 
-  connect_port - Convenience method to get an authenticated control connection
-  connect_socket_file - Similar to connect_port, but for control socket files
+  connect - Simple method for getting authenticated control connection
 
   authenticate - Main method for authenticating to a control socket
   authenticate_none - Authenticates to an open control socket
@@ -149,6 +147,93 @@ AuthMethod = stem.util.enum.Enum("NONE", "PASSWORD", "COOKIE", "SAFECOOKIE", "UN
 CLIENT_HASH_CONSTANT = b"Tor safe cookie authentication controller-to-server hash"
 SERVER_HASH_CONSTANT = b"Tor safe cookie authentication server-to-controller hash"
 
+CONNECT_MESSAGES = {
+  'general_auth_failure': "Unable to authenticate: {error}",
+  'incorrect_password': "Incorrect password",
+  'no_control_port': "Unable to connect to tor. Maybe it's running without a ControlPort?",
+  'password_prompt': "Tor controller password:",
+  'socket_doesnt_exist': "The socket file you specified ({path}) doesn't exist",
+  'tor_isnt_running': "Unable to connect to tor. Are you sure it's running?",
+  'unable_to_use_port': "Unable to connect to {address}:{port}: {error}",
+  'unable_to_use_socket': "Unable to connect to '{path}': {error}",
+}
+
+
+def connect(control_port = ('127.0.0.1', 9051), control_socket = '/var/run/tor/control', password = None, chroot_path = None, controller = stem.control.Controller):
+  """
+  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**.
+
+  If both a **control_port** and **control_socket** are provided then the
+  **control_socket** is tried first, and this provides a generic error message
+  if they're both unavailable.
+
+  In much the same vein as git porcelain commands, users should not rely on
+  details of how this works. Messages and details of this function's behavior
+  could change in the future.
+
+  :param tuple contol_port: address and port tuple, for instance **('127.0.0.1', 9051)**
+  :param str path: path where the control socket is located
+  :param str password: passphrase to authenticate to the socket
+  :param str chroot_path: path prefix if in a chroot environment
+  :param Class controller: :class:`~stem.control.BaseController` subclass to be
+    returned, this provides a :class:`~stem.socket.ControlSocket` if **None**
+
+  :returns: authenticated control connection, the type based on the controller argument
+
+  :raises: **ValueError** if given an invalid control_port, or both
+    **control_port** and **control_socket** are **None**
+  """
+
+  if control_port is None and control_socket is None:
+    raise ValueError("Neither a control port nor control socket were provided. Nothing to connect to.")
+  elif control_port:
+    if len(control_port) != 2:
+      raise ValueError("The control_port argument for connect() should be an (address, port) tuple.")
+    elif not stem.util.connection.is_valid_ipv4_address(control_port[0]):
+      raise ValueError("'%s' isn't a vaid IPv4 address" % control_port[0])
+    elif not stem.util.connection.is_valid_port(control_port[1]):
+      raise ValueError("'%s' isn't a valid port" % control_port[1])
+
+  control_connection, error_msg = None, ''
+
+  if control_socket:
+    if os.path.exists(control_socket):
+      try:
+        control_connection = stem.socket.ControlSocketFile(control_socket)
+      except stem.SocketError as exc:
+        error_msg = CONNECT_MESSAGES['unable_to_use_socket'].format(path = control_socket, error = exc)
+    else:
+      error_msg = CONNECT_MESSAGES['socket_doesnt_exist'].format(path = control_socket)
+
+  if control_port and not control_connection:
+    address, port = control_port
+
+    try:
+      control_connection = stem.socket.ControlPort(address, port)
+    except stem.SocketError as exc:
+      error_msg = CONNECT_MESSAGES['unable_to_use_port'].format(address = address, port = port, error = exc)
+
+  # If unable to connect to either a control socket or port then finally fail
+  # out. If we only attempted to connect to one of them then provide the error
+  # output from that. Otherwise we provide a more generic error message.
+  #
+  # We check for a 'tor.real' process name because that's what TBB uses.
+
+  if not control_connection:
+    if control_socket and control_port:
+      if not stem.util.system.is_running('tor') and not stem.util.system.is_running('tor.real'):
+        error_msg = CONNECT_MESSAGES['tor_isnt_running']
+      else:
+        error_msg = CONNECT_MESSAGES['no_control_port']
+
+    print error_msg
+    return None
+
+  return _connect(control_connection, password, chroot_path, controller)
+
 
 def connect_port(address = "127.0.0.1", port = 9051, password = None, chroot_path = None, controller = stem.control.Controller):
   """
@@ -157,6 +242,9 @@ def connect_port(address = "127.0.0.1", port = 9051, password = None, chroot_pat
   if necessary (and none is provided). If any issues arise this prints a
   description of the problem and returns **None**.
 
+  .. deprecated:: 1.2.0
+     Use :func:`~stem.connection.connect` instead.
+
   :param str address: ip address of the controller
   :param int port: port number of the controller
   :param str password: passphrase to authenticate to the socket
@@ -181,6 +269,13 @@ def connect_socket_file(path = "/var/run/tor/control", password = None, chroot_p
   Convenience function for quickly getting a control connection. For more
   information see the :func:`~stem.connection.connect_port` function.
 
+  In much the same vein as git porcelain commands, users should not rely on
+  details of how this works. Messages or details of this function's behavior
+  might change in the future.
+
+  .. deprecated:: 1.2.0
+     Use :func:`~stem.connection.connect` instead.
+
   :param str path: path where the control socket is located
   :param str password: passphrase to authenticate to the socket
   :param str chroot_path: path prefix if in a chroot environment
@@ -219,19 +314,41 @@ def _connect(control_socket, password, chroot_path, controller):
       return control_socket
     else:
       return controller(control_socket)
+  except IncorrectSocketType:
+    if isinstance(control_socket, stem.socket.ControlPort):
+      print CONNECT_MESSAGES['wrong_port_type'].format(port = control_socket.get_port())
+    else:
+      print CONNECT_MESSAGES['wrong_socket_type']
+
+    control_socket.close()
+    return None
+  except UnrecognizedAuthMethods as exc:
+    print CONNECT_MESSAGES['uncrcognized_auth_type'].format(auth_methods = ', '.join(exc.unknown_auth_methods))
+    control_socket.close()
+    return None
+  except IncorrectPassword:
+    print CONNECT_MESSAGES['incorrect_password']
+    control_socket.close()
+    return None
   except MissingPassword:
     if password is not None:
-      raise ValueError("BUG: authenticate raised MissingPassword despite getting one")
+      control_socket.close()
+      raise ValueError(CONNECT_MESSAGES['missing_password_bug'])
 
     try:
-      password = getpass.getpass("Controller password: ")
+      password = getpass.getpass(CONNECT_MESSAGES['password_prompt'] + ' ')
     except KeyboardInterrupt:
+      control_socket.close()
       return None
 
     return _connect(control_socket, password, chroot_path, controller)
+  except UnreadableCookieFile as exc:
+    print CONNECT_MESSAGES['unreadable_cookie_file'].format(path = exc.cookie_path, issue = str(exc))
+    control_socket.close()
+    return None
   except AuthenticationFailure as exc:
+    print CONNECT_MESSAGES['general_auth_failure'].format(error = exc)
     control_socket.close()
-    print "Unable to authenticate: %s" % exc
     return None
 
 
diff --git a/stem/socket.py b/stem/socket.py
index cb74f21..6a452c7 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -36,7 +36,7 @@ Tor...
 
 ::
 
-  % python example.py 
+  % python example.py
   Issuing 'GETINFO version' query...
 
   version=0.2.4.10-alpha-dev (git-8be6058d8f31e578)
diff --git a/test/integ/connection/connect.py b/test/integ/connection/connect.py
index 994b777..4361c0f 100644
--- a/test/integ/connection/connect.py
+++ b/test/integ/connection/connect.py
@@ -19,6 +19,25 @@ class TestConnect(unittest.TestCase):
   def tearDown(self):
     sys.stdout = self.original_stdout
 
+  def test_connect(self):
+    """
+    Basic sanity checks for the connect function.
+    """
+
+    if test.runner.require_control(self):
+      return
+
+    runner = test.runner.get_runner()
+
+    control_socket = stem.connection.connect(
+      control_port = ('127.0.0.1', test.runner.CONTROL_PORT),
+      control_socket = test.runner.CONTROL_SOCKET_PATH,
+      password = test.runner.CONTROL_PASSWORD,
+      chroot_path = runner.get_chroot(),
+      controller = None)
+
+    test.runner.exercise_controller(self, control_socket)
+
   def test_connect_port(self):
     """
     Basic sanity checks for the connect_port function.





More information about the tor-commits mailing list