[tor-commits] [stem/master] Specialized subclasses for ControlSocket

atagar at torproject.org atagar at torproject.org
Mon Nov 28 18:10:26 UTC 2011


commit 7bae33db31f26440810596b0b702d7f85bbfb1cd
Author: Damian Johnson <atagar at torproject.org>
Date:   Mon Nov 28 06:18:03 2011 -0800

    Specialized subclasses for ControlSocket
    
    Adding a ControlSocket subclass for control ports and control sockets. This
    allows for a connect() method which we'll need when trying multiple connection
    types since the socket becomes detached after a failed authentication attempt.
    This is also gonna be a bit nicer for callers since it bundles the connection
    information (the port/path we're using) with the socket.
---
 stem/connection.py |   66 +++++++++-----------
 stem/control.py    |    5 ++
 stem/socket.py     |  176 ++++++++++++++++++++++++++++++++++++++++++++--------
 3 files changed, 184 insertions(+), 63 deletions(-)

diff --git a/stem/connection.py b/stem/connection.py
index cb8425a..fe17d6b 100644
--- a/stem/connection.py
+++ b/stem/connection.py
@@ -14,11 +14,7 @@ ProtocolInfoResponse - Reply from a PROTOCOLINFO query.
   +- convert - parses a ControlMessage, turning it into a ProtocolInfoResponse
 """
 
-from __future__ import absolute_import
-import Queue
-import socket
 import logging
-import threading
 
 import stem.socket
 import stem.version
@@ -62,14 +58,24 @@ def get_protocolinfo_by_port(control_addr = "127.0.0.1", control_port = 9051, ke
       socket
   """
   
-  control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-  connection_args = (control_addr, control_port)
-  protocolinfo_response = _get_protocolinfo_impl(control_socket, connection_args, keep_alive)
-  
-  # attempt to expand relative cookie paths using our port to infer the pid
-  protocolinfo_response.cookie_path = _expand_cookie_path(protocolinfo_response.cookie_path, stem.util.system.get_pid_by_port, control_port)
-  
-  return protocolinfo_response
+  try:
+    control_socket = stem.socket.ControlPort(control_addr, control_port)
+    control_socket.connect()
+    control_socket.send("PROTOCOLINFO 1")
+    protocolinfo_response = control_socket.recv()
+    ProtocolInfoResponse.convert(protocolinfo_response)
+    
+    if keep_alive: protocolinfo_response.socket = control_socket
+    else: control_socket.close()
+    
+    # attempt to expand relative cookie paths using our port to infer the pid
+    if control_addr == "127.0.0.1":
+      _expand_cookie_path(protocolinfo_response, stem.util.system.get_pid_by_port, control_port)
+    
+    return protocolinfo_response
+  except stem.socket.ControllerError, exc:
+    control_socket.close()
+    raise exc
 
 def get_protocolinfo_by_socket(socket_path = "/var/run/tor/control", keep_alive = False):
   """
@@ -87,27 +93,9 @@ def get_protocolinfo_by_socket(socket_path = "/var/run/tor/control", keep_alive
       socket
   """
   
-  control_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-  protocolinfo_response = _get_protocolinfo_impl(control_socket, socket_path, keep_alive)
-  
-  # attempt to expand relative cookie paths using our socket to infer the pid
-  protocolinfo_response.cookie_path = _expand_cookie_path(protocolinfo_response.cookie_path, stem.util.system.get_pid_by_open_file, socket_path)
-  
-  return protocolinfo_response
-
-def _get_protocolinfo_impl(control_socket, connection_args, keep_alive):
-  """
-  Common implementation behind the get_protocolinfo_by_* functions. This
-  connects the given socket and issues a PROTOCOLINFO query with it.
-  """
-  
-  try:
-    control_socket.connect(connection_args)
-    control_socket = stem.socket.ControlSocket(control_socket)
-  except socket.error, exc:
-    raise stem.socket.SocketError(exc)
-  
   try:
+    control_socket = stem.socket.ControlSocketFile(socket_path)
+    control_socket.connect()
     control_socket.send("PROTOCOLINFO 1")
     protocolinfo_response = control_socket.recv()
     ProtocolInfoResponse.convert(protocolinfo_response)
@@ -115,18 +103,22 @@ def _get_protocolinfo_impl(control_socket, connection_args, keep_alive):
     if keep_alive: protocolinfo_response.socket = control_socket
     else: control_socket.close()
     
+    # attempt to expand relative cookie paths using our port to infer the pid
+    _expand_cookie_path(protocolinfo_response, stem.util.system.get_pid_by_open_file, socket_path)
+    
     return protocolinfo_response
   except stem.socket.ControllerError, exc:
     control_socket.close()
     raise exc
 
-def _expand_cookie_path(cookie_path, pid_resolver, pid_resolution_arg):
+def _expand_cookie_path(protocolinfo_response, pid_resolver, pid_resolution_arg):
   """
   Attempts to expand a relative cookie path with the given pid resolver. This
-  returns the input path if it's already absolute, None, or the system calls
-  fail.
+  leaves the cookie_path alone if it's already absolute, None, or the system
+  calls fail.
   """
   
+  cookie_path = protocolinfo_response.cookie_path
   if cookie_path and stem.util.system.is_relative_path(cookie_path):
     try:
       tor_pid = pid_resolver(pid_resolution_arg)
@@ -146,7 +138,7 @@ def _expand_cookie_path(cookie_path, pid_resolver, pid_resolution_arg):
       pid_resolver_label = resolver_labels.get(pid_resolver, "")
       LOGGER.debug("unable to expand relative tor cookie path%s: %s" % (pid_resolver_label, exc))
   
-  return cookie_path
+  protocolinfo_response.cookie_path = cookie_path
 
 class ProtocolInfoResponse(stem.socket.ControlMessage):
   """
@@ -276,7 +268,7 @@ class ProtocolInfoResponse(stem.socket.ControlMessage):
           self.cookie_path = line.pop_mapping(True, True)[1]
           
           # attempt to expand relative cookie paths
-          self.cookie_path = _expand_cookie_path(self.cookie_path, stem.util.system.get_pid_by_name, "tor")
+          _expand_cookie_path(self, stem.util.system.get_pid_by_name, "tor")
       elif line_type == "VERSION":
         # Line format:
         #   VersionLine = "250-VERSION" SP "Tor=" TorVersion OptArguments CRLF
diff --git a/stem/control.py b/stem/control.py
index f372b57..3d55b62 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -1,6 +1,11 @@
 # The following is very much a work in progress and mostly scratch (I just
 # wanted to make sure other work would nicely do the async event handling).
 
+import Queue
+import threading
+
+import stem.socket
+
 class ControlConnection:
   """
   Connection to a Tor control port. This is a very lightweight wrapper around
diff --git a/stem/socket.py b/stem/socket.py
index 8add15f..0d2a84e 100644
--- a/stem/socket.py
+++ b/stem/socket.py
@@ -9,9 +9,17 @@ ControllerError - Base exception raised when using the controller.
      +- SocketClosed - Socket has been shut down.
 
 ControlSocket - Socket wrapper that speaks the tor control protocol.
+  |- ControlPort - Control connection via a port.
+  |  |- get_address - provides the ip address of our socket
+  |  +- get_port - provides the port of our socket
+  |
+  |- ControlSocketFile - Control connection via a local file socket.
+  |  +- get_socket_path - provides the path of the socket we connect to
+  |
   |- send - sends a message to the socket
   |- recv - receives a ControlMessage from the socket
   |- is_alive - reports if the socket is known to be closed
+  |- connect - connects a new socket
   +- close - shuts down the socket
 
 ControlMessage - Message that's read from the control socket.
@@ -74,20 +82,14 @@ class ControlSocket:
   Wrapper for a socket connection that speaks the Tor control protocol. To the
   better part this transparently handles the formatting for sending and
   receiving complete messages. All methods are thread safe.
+  
+  Callers should not instantiate this class directly, but rather use subclasses
+  which are expected to implement the _make_socket method.
   """
   
-  def __init__(self, control_socket):
-    """
-    Constructs as a wrapper around an established socket connection. Further
-    interaction with the raw socket is discouraged.
-    
-    Arguments:
-      control_socket (socket.socket) - established tor control socket
-    """
-    
-    self._socket = control_socket
-    self._socket_file = control_socket.makefile()
-    self._is_alive = True
+  def __init__(self):
+    self._socket, self._socket_file = None, None
+    self._is_alive = False
     
     # Tracks sending and receiving separately. This should be safe, and doing
     # so prevents deadlock where we block writes because we're waiting to read
@@ -162,35 +164,157 @@ class ControlSocket:
     
     return self._is_alive
   
-  def close(self):
+  def connect(self):
     """
-    Shuts down the socket. If it's already closed then this is a no-op.
+    Connects to a new socket, closing our previous one if we're already
+    attached.
+    
+    Raises:
+      stem.socket.SocketError if unable to make a socket
     """
     
     # we need both locks for this
     self._send_cond.acquire()
     self._recv_cond.acquire()
     
-    # if we haven't yet established a connection then this raises an error
-    # socket.error: [Errno 107] Transport endpoint is not connected
-    try: self._socket.shutdown(socket.SHUT_RDWR)
-    except socket.error: pass
+    # close the socket if we're currently attached to one
+    if self.is_alive(): self.close()
     
-    # Suppressing unexpected exceptions from close. For instance, if the
-    # socket's file has already been closed then with python 2.7 that raises
-    # with...
-    # error: [Errno 32] Broken pipe
+    try:
+      control_socket = self._make_socket()
+      self._socket = control_socket
+      self._socket_file = control_socket.makefile()
+      self._is_alive = True
+    finally:
+      self._send_cond.release()
+      self._recv_cond.release()
+  
+  def close(self):
+    """
+    Shuts down the socket. If it's already closed then this is a no-op.
+    """
     
-    try: self._socket.close()
-    except: pass
+    # we need both locks for this
+    self._send_cond.acquire()
+    self._recv_cond.acquire()
+    
+    if self._socket:
+      # if we haven't yet established a connection then this raises an error
+      # socket.error: [Errno 107] Transport endpoint is not connected
+      try: self._socket.shutdown(socket.SHUT_RDWR)
+      except socket.error: pass
+      
+      # Suppressing unexpected exceptions from close. For instance, if the
+      # socket's file has already been closed then with python 2.7 that raises
+      # with...
+      # error: [Errno 32] Broken pipe
+      
+      try: self._socket.close()
+      except: pass
     
-    try: self._socket_file.close()
-    except: pass
+    if self._socket_file:
+      try: self._socket_file.close()
+      except: pass
     
     self._is_alive = False
     
     self._send_cond.release()
     self._recv_cond.release()
+  
+  def _make_socket(self):
+    """
+    Constructs and connects new socket. This is implemented by subclasses.
+    
+    Returns:
+      socket.socket for our configuration
+    
+    Raises:
+      stem.socket.SocketError if unable to make a socket
+    """
+    
+    raise SocketError("Unsupported Operation: this should be implemented by the ControlSocket subclass")
+
+class ControlPort(ControlSocket):
+  """
+  Control connection to tor. For more information see tor's ControlPort torrc
+  option.
+  """
+  
+  def __init__(self, control_addr = "127.0.0.1", control_port = 9051):
+    """
+    ControlPort constructor.
+    
+    Arguments:
+      control_addr (str) - ip address of the controller
+      control_port (int) - port number of the controller
+    """
+    
+    ControlSocket.__init__(self)
+    self._control_addr = control_addr
+    self._control_port = control_port
+  
+  def get_address(self):
+    """
+    Provides the ip address our socket connects to.
+    
+    Returns:
+      str with the ip address of our socket
+    """
+    
+    return self._control_addr
+  
+  def get_port(self):
+    """
+    Provides the port our socket connects to.
+    
+    Returns:
+      int with the port of our socket
+    """
+    
+    return self._control_port
+  
+  def _make_socket(self):
+    try:
+      control_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+      control_socket.connect((self._control_addr, self._control_port))
+      return control_socket
+    except socket.error, exc:
+      raise SocketError(exc)
+
+class ControlSocketFile(ControlSocket):
+  """
+  Control connection to tor. For more information see tor's ControlSocket torrc
+  option.
+  """
+  
+  def __init__(self, socket_path = "/var/run/tor/control"):
+    """
+    ControlSocketFile constructor.
+    
+    Arguments:
+      socket_path (str) - path where the control socket is located
+    """
+    
+    ControlSocket.__init__(self)
+    self._socket_path = socket_path
+  
+  def get_socket_path(self):
+    """
+    Provides the path our socket connects to.
+    
+    Returns:
+      str with the path for our control socket
+    """
+    
+    return self._socket_path
+  
+  def _make_socket(self):
+    try:
+      control_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+      control_socket.connect(self._socket_path)
+      return control_socket
+    except socket.error, exc:
+      raise SocketError(exc)
 
 class ControlMessage:
   """





More information about the tor-commits mailing list