commit 86de12ecab643b809d2e75265512e0dc4267dd12
Author: Damian Johnson <atagar(a)torproject.org>
Date: Sun Dec 30 16:23:42 2012 -0800
Reordering Controller methods
Rearranging the Controller's methods to match the header. This'll both make it
easier to find things and rearrange our documentation.
---
stem/control.py | 577 +++++++++++++++++++++++++++----------------------------
1 files changed, 288 insertions(+), 289 deletions(-)
diff --git a/stem/control.py b/stem/control.py
index fdcf332..73f0faf 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -15,19 +15,18 @@ providing its own for interacting at a higher level.
|
|- authenticate - authenticates this controller with tor
|
+ |- get_info - issues a GETINFO query for a parameter
|- get_version - convenience method to get tor version
|- get_socks_listeners - provides where tor is listening for SOCKS connections
|- get_protocolinfo - information about the controller interface
|
- |- get_info - issues a GETINFO query for a parameter
- |- get_conf - gets the value of a configuration option
- |- get_conf_map - gets the values of multiple configuration options
- |
|- get_server_descriptor - querying the server descriptor for a relay
|- get_server_descriptors - provides all presently available server descriptors
|- get_network_status - querying the router status entry for a relay
|- get_network_statuses - provides all preently available router status entries
|
+ |- get_conf - gets the value of a configuration option
+ |- get_conf_map - gets the values of multiple configuration options
|- set_conf - sets the value of a configuration option
|- reset_conf - reverts configuration options to their default values
|- set_options - sets or resets the values of multiple configuration options
@@ -35,12 +34,12 @@ providing its own for interacting at a higher level.
|- add_event_listener - attaches an event listener to be notified of tor events
|- remove_event_listener - removes a listener so it isn't notified of further events
|
- |- load_conf - loads configuration information as if it was in the torrc
- |- save_conf - saves configuration information to the torrc
- |
|- is_caching_enabled - true if the controller has enabled caching
|- clear_cache - clears any cached results
|
+ |- load_conf - loads configuration information as if it was in the torrc
+ |- save_conf - saves configuration information to the torrc
+ |
|- is_feature_enabled - checks if a given controller feature is enabled
|- enable_feature - enables a controller feature that has been disabled by default
|
@@ -666,101 +665,14 @@ class Controller(BaseController):
super(Controller, self).close()
- def add_event_listener(self, listener, *events):
- """
- Directs further tor controller events to a given function. The function is
- expected to take a single argument, which is a
- :class:`~stem.response.events.Event` subclass. For instance the following
- would print the bytes sent and received by tor over five seconds...
-
- ::
-
- import time
- from stem.control import Controller, EventType
-
- def print_bw(event):
- print "sent: %i, received: %i" % (event.written, event.read)
-
- with Controller.from_port(control_port = 9051) as controller:
- controller.authenticate()
- controller.add_event_listener(print_bw, EventType.BW)
- time.sleep(5)
-
- :param functor listener: function to be called when an event is received
- :param stem.control.EventType events: event types to be listened for
-
- :raises: :class:`stem.ProtocolError` if unable to set the events
- """
-
- # first checking that tor supports these event types
- for event_type in events:
- event_version = stem.response.events.EVENT_TYPE_TO_CLASS[event_type]._VERSION_ADDED
- if not self.get_version().meets_requirements(event_version):
- raise stem.InvalidRequest(552, "%s event requires Tor version %s or later" % (event_type, event_version))
-
- with self._event_listeners_lock:
- for event_type in events:
- self._event_listeners.setdefault(event_type, []).append(listener)
-
- self._attach_listeners()
-
- def remove_event_listener(self, listener):
- """
- Stops a listener from being notified of further tor events.
-
- :param stem.control.EventListener listener: listener to be removed
-
- :raises: :class:`stem.ProtocolError` if unable to set the events
- """
-
- with self._event_listeners_lock:
- event_types_changed = False
-
- for event_type, event_listeners in self._event_listeners.items():
- if listener in event_listeners:
- event_listeners.remove(listener)
-
- if len(event_listeners) == 0:
- event_types_changed = True
- del self._event_listeners[event_type]
-
- if event_types_changed:
- response = self.msg("SETEVENTS %s" % " ".join(self._event_listeners.keys()))
-
- if not response.is_ok():
- raise stem.ProtocolError("SETEVENTS received unexpected response\n%s" % response)
-
- def is_caching_enabled(self):
- """
- **True** if caching has been enabled, **False** otherwise.
-
- :returns: bool to indicate if caching is enabled
- """
-
- return self._is_caching_enabled
-
- def is_geoip_unavailable(self):
- """
- Provides **True** if we've concluded hat our geoip database is unavailable,
- **False** otherwise. This is determined by having our 'GETINFO
- ip-to-country/\*' lookups fail so this will default to **False** if we
- aren't making those queries.
-
- Geoip failures will be untracked if caching is disabled.
-
- :returns: **bool** to indicate if we've concluded our geoip database to be
- unavailable or not
- """
-
- return self._geoip_failure_count >= GEOIP_FAILURE_THRESHOLD
-
- def clear_cache(self):
+ def authenticate(self, *args, **kwargs):
"""
- Drops any cached results.
+ A convenience method to authenticate the controller. This is just a
+ pass-through to :func:`stem.connection.authenticate`.
"""
- self._request_cache = {}
- self._geoip_failure_count = 0
+ import stem.connection
+ stem.connection.authenticate(self, *args, **kwargs)
def get_info(self, params, default = UNDEFINED):
"""
@@ -890,6 +802,81 @@ class Controller(BaseController):
if default == UNDEFINED: raise exc
else: return default
+ def get_socks_listeners(self, default = UNDEFINED):
+ """
+ Provides the SOCKS **(address, port)** tuples that tor has open.
+
+ :param object default: response if the query fails
+
+ :returns: list of **(address, port)** tuples for the available SOCKS
+ listeners
+
+ :raises: :class:`stem.ControllerError` if unable to determine the listeners
+ and no default was provided
+ """
+
+ try:
+ proxy_addrs = []
+
+ try:
+ for listener in self.get_info("net/listeners/socks").split():
+ if not (listener.startswith('"') and listener.endswith('"')):
+ raise stem.ProtocolError("'GETINFO net/listeners/socks' responses are expected to be quoted: %s" % listener)
+ elif not ':' in listener:
+ raise stem.ProtocolError("'GETINFO net/listeners/socks' had a listener without a colon: %s" % listener)
+
+ listener = listener[1:-1] # strip quotes
+ addr, port = listener.split(':')
+ proxy_addrs.append((addr, port))
+ except stem.InvalidArguments:
+ # tor version is old (pre-tor-0.2.2.26-beta), use get_conf() instead
+ socks_port = self.get_conf('SocksPort')
+
+ for listener in self.get_conf('SocksListenAddress', multiple = True):
+ if ':' in listener:
+ addr, port = listener.split(':')
+ proxy_addrs.append((addr, port))
+ else:
+ proxy_addrs.append((listener, socks_port))
+
+ # validate that address/ports are valid, and convert ports to ints
+
+ for addr, port in proxy_addrs:
+ if not stem.util.connection.is_valid_ip_address(addr):
+ raise stem.ProtocolError("Invalid address for a SOCKS listener: %s" % addr)
+ elif not stem.util.connection.is_valid_port(port):
+ raise stem.ProtocolError("Invalid port for a SOCKS listener: %s" % port)
+
+ return [(addr, int(port)) for (addr, port) in proxy_addrs]
+ except Exception, exc:
+ if default == UNDEFINED: raise exc
+ else: return default
+
+ def get_protocolinfo(self, default = UNDEFINED):
+ """
+ A convenience method to get the protocol info of the controller.
+
+ :param object default: response if the query fails
+
+ :returns: :class:`~stem.response.protocolinfo.ProtocolInfoResponse` provided by tor
+
+ :raises:
+ * :class:`stem.ProtocolError` if the PROTOCOLINFO response is
+ malformed
+ * :class:`stem.SocketError` if problems arise in establishing or
+ using the socket
+
+ An exception is only raised if we weren't provided a default response.
+ """
+
+ import stem.connection
+
+ try:
+ return stem.connection.get_protocolinfo(self)
+ except Exception, exc:
+ if default == UNDEFINED: raise exc
+ else: return default
+
def get_server_descriptor(self, relay, default = UNDEFINED):
"""
Provides the server descriptor for the relay with the given fingerprint or
@@ -1023,40 +1010,6 @@ class Controller(BaseController):
for entry in default:
yield entry
- def authenticate(self, *args, **kwargs):
- """
- A convenience method to authenticate the controller. This is just a
- pass-through to :func:`stem.connection.authenticate`.
- """
-
- import stem.connection
- stem.connection.authenticate(self, *args, **kwargs)
-
- def get_protocolinfo(self, default = UNDEFINED):
- """
- A convenience method to get the protocol info of the controller.
-
- :param object default: response if the query fails
-
- :returns: :class:`~stem.response.protocolinfo.ProtocolInfoResponse` provided by tor
-
- :raises:
- * :class:`stem.ProtocolError` if the PROTOCOLINFO response is
- malformed
- * :class:`stem.SocketError` if problems arise in establishing or
- using the socket
-
- An exception is only raised if we weren't provided a default response.
- """
-
- import stem.connection
-
- try:
- return stem.connection.get_protocolinfo(self)
- except Exception, exc:
- if default == UNDEFINED: raise exc
- else: return default
-
def get_conf(self, param, default = UNDEFINED, multiple = False):
"""
Queries the current value for a configuration option. Some configuration
@@ -1347,31 +1300,112 @@ class Controller(BaseController):
else:
raise stem.ProtocolError("Returned unexpected status code: %s" % response.code)
- def load_conf(self, configtext):
+ def add_event_listener(self, listener, *events):
"""
- Sends the configuration text to Tor and loads it as if it has been read from
- the torrc.
+ Directs further tor controller events to a given function. The function is
+ expected to take a single argument, which is a
+ :class:`~stem.response.events.Event` subclass. For instance the following
+ would print the bytes sent and received by tor over five seconds...
- :param str configtext: the configuration text
+ ::
- :raises: :class:`stem.ControllerError` if the call fails
- """
+ import time
+ from stem.control import Controller, EventType
+
+ def print_bw(event):
+ print "sent: %i, received: %i" % (event.written, event.read)
+
+ with Controller.from_port(control_port = 9051) as controller:
+ controller.authenticate()
+ controller.add_event_listener(print_bw, EventType.BW)
+ time.sleep(5)
- response = self.msg("LOADCONF\n%s" % configtext)
- stem.response.convert("SINGLELINE", response)
+ :param functor listener: function to be called when an event is received
+ :param stem.control.EventType events: event types to be listened for
- if response.code in ("552", "553"):
- if response.code == "552" and response.message.startswith("Invalid config file: Failed to parse/validate config: Unknown option"):
- raise stem.InvalidArguments(response.code, response.message, [response.message[70:response.message.find('.', 70) - 1]])
- raise stem.InvalidRequest(response.code, response.message)
- elif not response.is_ok():
- raise stem.ProtocolError("+LOADCONF Received unexpected response\n%s" % str(response))
-
- def save_conf(self):
+ :raises: :class:`stem.ProtocolError` if unable to set the events
"""
- Saves the current configuration options into the active torrc file.
- :raises:
+ # first checking that tor supports these event types
+ for event_type in events:
+ event_version = stem.response.events.EVENT_TYPE_TO_CLASS[event_type]._VERSION_ADDED
+ if not self.get_version().meets_requirements(event_version):
+ raise stem.InvalidRequest(552, "%s event requires Tor version %s or later" % (event_type, event_version))
+
+ with self._event_listeners_lock:
+ for event_type in events:
+ self._event_listeners.setdefault(event_type, []).append(listener)
+
+ self._attach_listeners()
+
+ def remove_event_listener(self, listener):
+ """
+ Stops a listener from being notified of further tor events.
+
+ :param stem.control.EventListener listener: listener to be removed
+
+ :raises: :class:`stem.ProtocolError` if unable to set the events
+ """
+
+ with self._event_listeners_lock:
+ event_types_changed = False
+
+ for event_type, event_listeners in self._event_listeners.items():
+ if listener in event_listeners:
+ event_listeners.remove(listener)
+
+ if len(event_listeners) == 0:
+ event_types_changed = True
+ del self._event_listeners[event_type]
+
+ if event_types_changed:
+ response = self.msg("SETEVENTS %s" % " ".join(self._event_listeners.keys()))
+
+ if not response.is_ok():
+ raise stem.ProtocolError("SETEVENTS received unexpected response\n%s" % response)
+
+ def is_caching_enabled(self):
+ """
+ **True** if caching has been enabled, **False** otherwise.
+
+ :returns: bool to indicate if caching is enabled
+ """
+
+ return self._is_caching_enabled
+
+ def clear_cache(self):
+ """
+ Drops any cached results.
+ """
+
+ self._request_cache = {}
+ self._geoip_failure_count = 0
+
+ def load_conf(self, configtext):
+ """
+ Sends the configuration text to Tor and loads it as if it has been read from
+ the torrc.
+
+ :param str configtext: the configuration text
+
+ :raises: :class:`stem.ControllerError` if the call fails
+ """
+
+ response = self.msg("LOADCONF\n%s" % configtext)
+ stem.response.convert("SINGLELINE", response)
+
+ if response.code in ("552", "553"):
+ if response.code == "552" and response.message.startswith("Invalid config file: Failed to parse/validate config: Unknown option"):
+ raise stem.InvalidArguments(response.code, response.message, [response.message[70:response.message.find('.', 70) - 1]])
+ raise stem.InvalidRequest(response.code, response.message)
+ elif not response.is_ok():
+ raise stem.ProtocolError("+LOADCONF Received unexpected response\n%s" % str(response))
+
+ def save_conf(self):
+ """
+ Saves the current configuration options into the active torrc file.
+
+ :raises:
* :class:`stem.ControllerError` if the call fails
* :class:`stem.OperationFailed` if the client is unable to save
the configuration file
@@ -1387,56 +1421,6 @@ class Controller(BaseController):
else:
raise stem.ProtocolError("SAVECONF returned unexpected response code")
- def get_socks_listeners(self, default = UNDEFINED):
- """
- Provides the SOCKS **(address, port)** tuples that tor has open.
-
- :param object default: response if the query fails
-
- :returns: list of **(address, port)** tuples for the available SOCKS
- listeners
-
- :raises: :class:`stem.ControllerError` if unable to determine the listeners
- and no default was provided
- """
-
- try:
- proxy_addrs = []
-
- try:
- for listener in self.get_info("net/listeners/socks").split():
- if not (listener.startswith('"') and listener.endswith('"')):
- raise stem.ProtocolError("'GETINFO net/listeners/socks' responses are expected to be quoted: %s" % listener)
- elif not ':' in listener:
- raise stem.ProtocolError("'GETINFO net/listeners/socks' had a listener without a colon: %s" % listener)
-
- listener = listener[1:-1] # strip quotes
- addr, port = listener.split(':')
- proxy_addrs.append((addr, port))
- except stem.InvalidArguments:
- # tor version is old (pre-tor-0.2.2.26-beta), use get_conf() instead
- socks_port = self.get_conf('SocksPort')
-
- for listener in self.get_conf('SocksListenAddress', multiple = True):
- if ':' in listener:
- addr, port = listener.split(':')
- proxy_addrs.append((addr, port))
- else:
- proxy_addrs.append((listener, socks_port))
-
- # validate that address/ports are valid, and convert ports to ints
-
- for addr, port in proxy_addrs:
- if not stem.util.connection.is_valid_ip_address(addr):
- raise stem.ProtocolError("Invalid address for a SOCKS listener: %s" % addr)
- elif not stem.util.connection.is_valid_port(port):
- raise stem.ProtocolError("Invalid port for a SOCKS listener: %s" % port)
-
- return [(addr, int(port)) for (addr, port) in proxy_addrs]
- except Exception, exc:
- if default == UNDEFINED: raise exc
- else: return default
-
def is_feature_enabled(self, feature):
"""
Checks if a control connection feature is enabled. These features can be
@@ -1502,44 +1486,56 @@ class Controller(BaseController):
self._enabled_features += [entry.upper() for entry in features]
- def signal(self, signal):
+ def get_circuit(self, circuit_id, default = UNDEFINED):
"""
- Sends a signal to the Tor client.
+ Provides a circuit presently available from tor.
- :param stem.Signal signal: type of signal to be sent
+ :param int circuit_id: circuit to be fetched
+ :param object default: response if the query fails
- :raises: :class:`stem.InvalidArguments` if signal provided wasn't recognized
- """
+ :returns: :class:`stem.events.CircuitEvent` for the given circuit
- response = self.msg("SIGNAL %s" % signal)
- stem.response.convert("SINGLELINE", response)
+ :raises:
+ * :class:`stem.ControllerError` if the call fails
+ * ValueError if the circuit doesn't exist
+
+ An exception is only raised if we weren't provided a default response.
+ """
- if not response.is_ok():
- if response.code == "552":
- raise stem.InvalidArguments(response.code, response.message, [signal])
+ try:
+ for circ in self.get_circuits():
+ if circ.id == circuit_id:
+ return circ
- raise stem.ProtocolError("SIGNAL response contained unrecognized status code: %s" % response.code)
+ raise ValueError("Tor presently does not have a circuit with the id of '%s'" % circuit_id)
+ except Exception, exc:
+ if default == UNDEFINED: raise exc
+ else: return default
- def repurpose_circuit(self, circuit_id, purpose):
+ def get_circuits(self, default = UNDEFINED):
"""
- Changes a circuit's purpose. Currently, two purposes are recognized...
- * general
- * controller
+ Provides tor's currently available circuits.
- :param str circuit_id: id of the circuit whose purpose is to be changed
- :param str purpose: purpose (either "general" or "controller")
+ :param object default: response if the query fails
- :raises: :class:`stem.InvalidArguments` if the circuit doesn't exist or if the purpose was invalid
- """
+ :returns: **list** of :class:`stem.events.CircuitEvent` for our circuits
- response = self.msg("SETCIRCUITPURPOSE %s purpose=%s" % (circuit_id, purpose))
- stem.response.convert("SINGLELINE", response)
+ :raises: :class:`stem.ControllerError` if the call fails and no default was provided
+ """
- if not response.is_ok():
- if response.code == "552":
- raise stem.InvalidRequest(response.code, response.message)
- else:
- raise stem.ProtocolError("SETCIRCUITPURPOSE returned unexpected response code: %s" % response.code)
+ try:
+ circuits = []
+ response = self.get_info("circuit-status")
+
+ for circ in response.splitlines():
+ circ_message = stem.socket.recv_message(StringIO.StringIO("650 CIRC " + circ + "\r\n"))
+ stem.response.convert("EVENT", circ_message, arrived_at = 0)
+ circuits.append(circ_message)
+
+ return circuits
+ except Exception, exc:
+ if default == UNDEFINED: raise exc
+ else: return default
def new_circuit(self, path = None, purpose = "general"):
"""
@@ -1614,56 +1610,49 @@ class Controller(BaseController):
return new_circuit
- def get_circuit(self, circuit_id, default = UNDEFINED):
+ def repurpose_circuit(self, circuit_id, purpose):
"""
- Provides a circuit presently available from tor.
-
- :param int circuit_id: circuit to be fetched
- :param object default: response if the query fails
+ Changes a circuit's purpose. Currently, two purposes are recognized...
+ * general
+ * controller
- :returns: :class:`stem.events.CircuitEvent` for the given circuit
+ :param str circuit_id: id of the circuit whose purpose is to be changed
+ :param str purpose: purpose (either "general" or "controller")
- :raises:
- * :class:`stem.ControllerError` if the call fails
- * ValueError if the circuit doesn't exist
-
- An exception is only raised if we weren't provided a default response.
+ :raises: :class:`stem.InvalidArguments` if the circuit doesn't exist or if the purpose was invalid
"""
- try:
- for circ in self.get_circuits():
- if circ.id == circuit_id:
- return circ
-
- raise ValueError("Tor presently does not have a circuit with the id of '%s'" % circuit_id)
- except Exception, exc:
- if default == UNDEFINED: raise exc
- else: return default
+ response = self.msg("SETCIRCUITPURPOSE %s purpose=%s" % (circuit_id, purpose))
+ stem.response.convert("SINGLELINE", response)
+
+ if not response.is_ok():
+ if response.code == "552":
+ raise stem.InvalidRequest(response.code, response.message)
+ else:
+ raise stem.ProtocolError("SETCIRCUITPURPOSE returned unexpected response code: %s" % response.code)
- def get_circuits(self, default = UNDEFINED):
+ def close_circuit(self, circuit_id, flag = ''):
"""
- Provides tor's currently available circuits.
-
- :param object default: response if the query fails
+ Closes the specified circuit.
- :returns: **list** of :class:`stem.events.CircuitEvent` for our circuits
+ :param str circuit_id: id of the circuit to be closed
+ :param str flag: optional value to modify closing, the only flag available
+ is "IfUnused" which will not close the circuit unless it is unused
- :raises: :class:`stem.ControllerError` if the call fails and no default was provided
+ :raises: :class:`stem.InvalidArguments` if the circuit is unknown
+ :raises: :class:`stem.InvalidRequest` if not enough information is provided
"""
- try:
- circuits = []
- response = self.get_info("circuit-status")
-
- for circ in response.splitlines():
- circ_message = stem.socket.recv_message(StringIO.StringIO("650 CIRC " + circ + "\r\n"))
- stem.response.convert("EVENT", circ_message, arrived_at = 0)
- circuits.append(circ_message)
-
- return circuits
- except Exception, exc:
- if default == UNDEFINED: raise exc
- else: return default
+ response = self.msg("CLOSECIRCUIT %s %s"% (str(circuit_id), flag))
+ stem.response.convert("SINGLELINE", response)
+
+ if not response.is_ok():
+ if response.code in ('512', '552'):
+ if response.message.startswith("Unknown circuit "):
+ raise stem.InvalidArguments(response.code, response.message, [circuit_id])
+ raise stem.InvalidRequest(response.code, response.message)
+ else:
+ raise stem.ProtocolError("CLOSECIRCUIT returned unexpected response code: %s" % response.code)
def attach_stream(self, stream_id, circuit_id, hop = None):
"""
@@ -1693,29 +1682,6 @@ class Controller(BaseController):
else:
raise stem.ProtocolError("ATTACHSTREAM returned unexpected response code: %s" % response.code)
- def close_circuit(self, circuit_id, flag = ''):
- """
- Closes the specified circuit.
-
- :param str circuit_id: id of the circuit to be closed
- :param str flag: optional value to modify closing, the only flag available
- is "IfUnused" which will not close the circuit unless it is unused
-
- :raises: :class:`stem.InvalidArguments` if the circuit is unknown
- :raises: :class:`stem.InvalidRequest` if not enough information is provided
- """
-
- response = self.msg("CLOSECIRCUIT %s %s"% (str(circuit_id), flag))
- stem.response.convert("SINGLELINE", response)
-
- if not response.is_ok():
- if response.code in ('512', '552'):
- if response.message.startswith("Unknown circuit "):
- raise stem.InvalidArguments(response.code, response.message, [circuit_id])
- raise stem.InvalidRequest(response.code, response.message)
- else:
- raise stem.ProtocolError("CLOSECIRCUIT returned unexpected response code: %s" % response.code)
-
def close_stream(self, stream_id, reason = stem.RelayEndReason.MISC, flag = ''):
"""
Closes the specified stream.
@@ -1744,6 +1710,39 @@ class Controller(BaseController):
else:
raise stem.ProtocolError("CLOSESTREAM returned unexpected response code: %s" % response.code)
+ def signal(self, signal):
+ """
+ Sends a signal to the Tor client.
+
+ :param stem.Signal signal: type of signal to be sent
+
+ :raises: :class:`stem.InvalidArguments` if signal provided wasn't recognized
+ """
+
+ response = self.msg("SIGNAL %s" % signal)
+ stem.response.convert("SINGLELINE", response)
+
+ if not response.is_ok():
+ if response.code == "552":
+ raise stem.InvalidArguments(response.code, response.message, [signal])
+
+ raise stem.ProtocolError("SIGNAL response contained unrecognized status code: %s" % response.code)
+
+ def is_geoip_unavailable(self):
+ """
+ Provides **True** if we've concluded hat our geoip database is unavailable,
+ **False** otherwise. This is determined by having our 'GETINFO
+ ip-to-country/\*' lookups fail so this will default to **False** if we
+ aren't making those queries.
+
+ Geoip failures will be untracked if caching is disabled.
+
+ :returns: **bool** to indicate if we've concluded our geoip database to be
+ unavailable or not
+ """
+
+ return self._geoip_failure_count >= GEOIP_FAILURE_THRESHOLD
+
def map_address(self, mapping):
"""
Map addresses to replacement addresses. Tor replaces subseqent connections