[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