commit 7bae33db31f26440810596b0b702d7f85bbfb1cd Author: Damian Johnson atagar@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: """
tor-commits@lists.torproject.org