commit 6c0962943c96a4683ce1634399289340f84b8b8a
Author: Damian Johnson <atagar(a)torproject.org>
Date: Mon Feb 24 09:13:01 2014 -0800
Controller methods get_ports() and get_listeners()
Determining tor's control or socks port was once as simple as...
GETCONF ControlPort
However, tor has expanded its config options so these often provide more
information than just the port number.
Back in commit 3a2875f Sean added a get_socks_listeners() method that uses
'GETINFO net/listeners/socks' to determine tor's socks port, and falls back on
config options if that's unavailable.
This change deprecates get_socks_listeners() in favor of more generic
get_ports() and get_listeners() methods. The get_listeners() behaves like
get_socks_listeners() but for several listener types, while get_ports()
provides just the port numbers for local connections.
---
docs/change_log.rst | 6 ++
stem/control.py | 139 ++++++++++++++++++++++++++++++++++----
test/integ/control/controller.py | 52 +++++++++++++-
test/unit/control/controller.py | 51 +++++++++++++-
4 files changed, 230 insertions(+), 18 deletions(-)
diff --git a/docs/change_log.rst b/docs/change_log.rst
index ab52e5f..dc43e4f 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -42,6 +42,7 @@ The following are only available within stem's `git repository
* **Controller**
* Added :func:`~stem.control.Controller.is_newnym_available` and :func:`~stem.control.Controller.get_newnym_wait` methods to the :class:`~stem.control.Controller`
+ * Added :func:`~stem.control.Controller.get_ports` and :func:`~stem.control.Controller.get_listeners` methods to the :class:`~stem.control.Controller`
* Added the id attribute to the :class:`~stem.response.events.ORConnEvent` (:spec:`6f2919a`)
* Added `support for CONN_BW events <api/response.html#stem.response.events.ConnectionBandwidthEvent>`_ (:spec:`6f2919a`)
* Added `support for CIRC_BW events <api/response.html#stem.response.events.CircuitBandwidthEvent>`_ (:spec:`6f2919a`)
@@ -53,6 +54,11 @@ The following are only available within stem's `git repository
* Added :func:`stem.util.connection.port_usage`
* Added :func:`stem.util.system.files_with_suffix`
+ * **Website**
+
+ * Expanded the `client usage tutorial <tutorials/to_russia_with_love.html>`_
+ to include an example for determining what exit tor uses for a connection.
+
.. _version_1.1:
Version 1.1
diff --git a/stem/control.py b/stem/control.py
index 0b611f5..9dff578 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -21,7 +21,8 @@ providing its own for interacting at a higher level.
|- get_info - issues a GETINFO query for a parameter
|- get_version - provides our tor version
|- get_exit_policy - provides our exit policy
- |- get_socks_listeners - provides where tor is listening for SOCKS connections
+ |- get_ports - provides the local ports where tor is listening for connections
+ |- get_listeners - provides the addresses and ports where tor is listening for connections
|- get_protocolinfo - information about the controller interface
|- get_user - provides the user tor is running as
|- get_pid - provides the pid of our tor process
@@ -135,6 +136,22 @@ providing its own for interacting at a higher level.
**STREAM_BW** :class:`stem.response.events.StreamBwEvent`
**WARN** :class:`stem.response.events.LogEvent`
===================== ===========
+
+.. data:: Listener (enum)
+
+ Purposes for inbound connections that Tor handles.
+
+ ============= ===========
+ Listener Description
+ ============= ===========
+ **OR** traffic we're relaying as a member of the network (torrc's **ORPort** and **ORListenAddress**)
+ **DIR** mirroring for tor descriptor content (torrc's **DirPort** and **DirListenAddress**)
+ **SOCKS** client traffic we're sending over Tor (torrc's **SocksPort** and **SocksListenAddress**)
+ **TRANS** transparent proxy handling (torrc's **TransPort** and **TransListenAddress**)
+ **NATD** forwarding for ipfw NATD connections (torrc's **NatdPort** and **NatdListenAddress**)
+ **DNS** DNS lookups for our traffic (torrc's **DNSPort** and **DNSListenAddress**)
+ **CONTROL** controller applications (torrc's **ControlPort** and **ControlListenAddress**)
+ ============= ===========
"""
import io
@@ -194,6 +211,16 @@ EventType = stem.util.enum.UppercaseEnum(
"CIRC_MINOR",
)
+Listener = stem.util.enum.UppercaseEnum(
+ "OR",
+ "DIR",
+ "SOCKS",
+ "TRANS",
+ "NATD",
+ "DNS",
+ "CONTROL",
+)
+
# Configuration options that are fetched by a special key. The keys are
# lowercase to make case insensitive lookups easier.
@@ -937,13 +964,44 @@ class Controller(BaseController):
else:
return default
- def get_socks_listeners(self, default = UNDEFINED):
+ def get_ports(self, listener_type, default = UNDEFINED):
"""
- Provides the SOCKS **(address, port)** tuples that tor has open.
+ Provides the local ports where tor is listening for the given type of
+ connections. This is similar to
+ :func:`~stem.control.Controller.get_listeners`, but doesn't provide
+ addresses nor include non-local endpoints.
+ :param stem.control.Listener listener_type: connection type being handled
+ by the ports we return
:param object default: response if the query fails
- :returns: list of **(address, port)** tuples for the available SOCKS
+ :returns: **list** of **ints** for the local ports where tor handles
+ connections of the given type
+
+ :raises: :class:`stem.ControllerError` if unable to determine the ports
+ and no default was provided
+ """
+
+ try:
+ return [port for (addr, port) in self.get_listeners(listener_type) if addr == '127.0.0.1']
+ except stem.ControllerError as exc:
+ if default == UNDEFINED:
+ raise exc
+ else:
+ return default
+
+ def get_listeners(self, listener_type, default = UNDEFINED):
+ """
+ Provides the addresses and ports where tor is listening for connections of
+ the given type. This is similar to
+ :func:`~stem.control.Controller.get_ports` but includes listener addresses
+ and non-local endpoints.
+
+ :param stem.control.Listener listener_type: connection type being handled
+ by the listeners we return
+ :param object default: response if the query fails
+
+ :returns: **list** of **(address, port)** tuples for the available
listeners
:raises: :class:`stem.ControllerError` if unable to determine the listeners
@@ -952,35 +1010,69 @@ class Controller(BaseController):
try:
proxy_addrs = []
+ query = 'net/listeners/%s' % listener_type.lower()
try:
- for listener in self.get_info("net/listeners/socks").split():
+ for listener in self.get_info(query).split():
if not (listener.startswith('"') and listener.endswith('"')):
- raise stem.ProtocolError("'GETINFO net/listeners/socks' responses are expected to be quoted: %s" % listener)
+ raise stem.ProtocolError("'GETINFO %s' responses are expected to be quoted: %s" % (query, listener))
elif not ':' in listener:
- raise stem.ProtocolError("'GETINFO net/listeners/socks' had a listener without a colon: %s" % listener)
+ raise stem.ProtocolError("'GETINFO %s' had a listener without a colon: %s" % (query, listener))
listener = listener[1:-1] # strip quotes
addr, port = listener.split(':')
+
+ # Skip unix sockets, for instance...
+ #
+ # GETINFO net/listeners/control
+ # 250-net/listeners/control="unix:/tmp/tor/socket"
+ # 250 OK
+
+ if addr == 'unix':
+ continue
+
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):
+ # Tor version is old (pre-tor-0.2.2.26-beta), use get_conf() instead.
+ # Some options (like the ORPort) can have optional attributes after the
+ # actual port number.
+
+ port_option = {
+ Listener.OR: 'ORPort',
+ Listener.DIR: 'DirPort',
+ Listener.SOCKS: 'SocksPort',
+ Listener.TRANS: 'TransPort',
+ Listener.NATD: 'NatdPort',
+ Listener.DNS: 'DNSPort',
+ Listener.CONTROL: 'ControlPort',
+ }[listener_type]
+
+ listener_option = {
+ Listener.OR: 'ORListenAddress',
+ Listener.DIR: 'DirListenAddress',
+ Listener.SOCKS: 'SocksListenAddress',
+ Listener.TRANS: 'TransListenAddress',
+ Listener.NATD: 'NatdListenAddress',
+ Listener.DNS: 'DNSListenAddress',
+ Listener.CONTROL: 'ControlListenAddress',
+ }[listener_type]
+
+ port_value = self.get_conf(port_option).split()[0]
+
+ for listener in self.get_conf(listener_option, multiple = True):
if ':' in listener:
addr, port = listener.split(':')
proxy_addrs.append((addr, port))
else:
- proxy_addrs.append((listener, socks_port))
+ proxy_addrs.append((listener, port_value))
# validate that address/ports are valid, and convert ports to ints
for addr, port in proxy_addrs:
if not stem.util.connection.is_valid_ipv4_address(addr):
- raise stem.ProtocolError("Invalid address for a SOCKS listener: %s" % addr)
+ raise stem.ProtocolError("Invalid address for a %s listener: %s" % (listener_type, addr))
elif not stem.util.connection.is_valid_port(port):
- raise stem.ProtocolError("Invalid port for a SOCKS listener: %s" % port)
+ raise stem.ProtocolError("Invalid port for a %s listener: %s" % (listener_type, port))
return [(addr, int(port)) for (addr, port) in proxy_addrs]
except Exception as exc:
@@ -989,6 +1081,25 @@ class Controller(BaseController):
else:
return default
+ def get_socks_listeners(self, default = UNDEFINED):
+ """
+ Provides the SOCKS **(address, port)** tuples that tor has open.
+
+ .. deprecated:: 1.2.0
+ Use :func:`~stem.control.Controller.get_listeners` with
+ **Listener.SOCKS** instead.
+
+ :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
+ """
+
+ return self.get_listeners(Listener.SOCKS, default)
+
def get_protocolinfo(self, default = UNDEFINED):
"""
A convenience method to get the protocol info of the controller.
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index d034853..e8bfe3e 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -22,7 +22,7 @@ import test.network
import test.runner
from stem import Flag, Signal
-from stem.control import EventType, State
+from stem.control import EventType, Listener, State
from stem.exit_policy import ExitPolicy
from stem.version import Requirement
@@ -593,9 +593,55 @@ class TestController(unittest.TestCase):
controller.save_conf()
controller.reset_conf("__OwningControllerProcess")
- def test_get_socks_ports(self):
+ def test_get_ports(self):
"""
- Test Controller.get_socks_ports against a running tor instance.
+ Test Controller.get_ports against a running tor instance.
+ """
+
+ if test.runner.require_control(self):
+ return
+
+ runner = test.runner.get_runner()
+
+ with runner.get_tor_controller() as controller:
+ self.assertEqual([], controller.get_ports(Listener.OR))
+ self.assertEqual([], controller.get_ports(Listener.DIR))
+ self.assertEqual([test.runner.SOCKS_PORT], controller.get_ports(Listener.SOCKS))
+ self.assertEqual([], controller.get_ports(Listener.TRANS))
+ self.assertEqual([], controller.get_ports(Listener.NATD))
+ self.assertEqual([], controller.get_ports(Listener.DNS))
+
+ if test.runner.Torrc.PORT in runner.get_options():
+ self.assertEqual([test.runner.CONTROL_PORT], controller.get_ports(Listener.CONTROL))
+ else:
+ self.assertEqual([], controller.get_ports(Listener.CONTROL))
+
+ def test_get_listeners(self):
+ """
+ Test Controller.get_listeners against a running tor instance.
+ """
+
+ if test.runner.require_control(self):
+ return
+
+ runner = test.runner.get_runner()
+
+ with runner.get_tor_controller() as controller:
+ self.assertEqual([], controller.get_listeners(Listener.OR))
+ self.assertEqual([], controller.get_listeners(Listener.DIR))
+ self.assertEqual([('127.0.0.1', test.runner.SOCKS_PORT)], controller.get_listeners(Listener.SOCKS))
+ self.assertEqual([], controller.get_listeners(Listener.TRANS))
+ self.assertEqual([], controller.get_listeners(Listener.NATD))
+ self.assertEqual([], controller.get_listeners(Listener.DNS))
+
+ if test.runner.Torrc.PORT in runner.get_options():
+ self.assertEqual([('127.0.0.1', test.runner.CONTROL_PORT)], controller.get_listeners(Listener.CONTROL))
+ else:
+ self.assertEqual([], controller.get_listeners(Listener.CONTROL))
+
+ def test_get_socks_listeners(self):
+ """
+ Test Controller.get_socks_listeners against a running tor instance.
"""
if test.runner.require_control(self):
diff --git a/test/unit/control/controller.py b/test/unit/control/controller.py
index 14dceea..171a781 100644
--- a/test/unit/control/controller.py
+++ b/test/unit/control/controller.py
@@ -13,7 +13,7 @@ import stem.util.system
import stem.version
from stem import InvalidArguments, InvalidRequest, ProtocolError, UnsatisfiableRequest
-from stem.control import _parse_circ_path, Controller, EventType
+from stem.control import _parse_circ_path, Listener, Controller, EventType
from stem.exit_policy import ExitPolicy
from test import mocking
@@ -126,6 +126,55 @@ class TestControl(unittest.TestCase):
@patch('stem.control.Controller.get_info')
@patch('stem.control.Controller.get_conf')
+ def test_get_ports(self, get_conf_mock, get_info_mock):
+ """
+ Exercises the get_ports() and get_listeners() methods.
+ """
+
+ # Exercise as an old version of tor that doesn't support the 'GETINFO
+ # net/listeners/*' options.
+
+ get_info_mock.side_effect = InvalidArguments
+
+ get_conf_mock.side_effect = lambda param, **kwargs: {
+ "ControlPort": "9050",
+ "ControlListenAddress": ["127.0.0.1"],
+ }[param]
+
+ self.assertEqual([('127.0.0.1', 9050)], self.controller.get_listeners(Listener.CONTROL))
+ self.assertEqual([9050], self.controller.get_ports(Listener.CONTROL))
+
+ # non-local addresss
+
+ get_conf_mock.side_effect = lambda param, **kwargs: {
+ "ControlPort": "9050",
+ "ControlListenAddress": ["27.4.4.1"],
+ }[param]
+
+ self.assertEqual([('27.4.4.1', 9050)], self.controller.get_listeners(Listener.CONTROL))
+ self.assertEqual([], self.controller.get_ports(Listener.CONTROL))
+
+ # Exercise via the GETINFO option.
+
+ get_info_mock.side_effect = None
+ get_info_mock.return_value = '"127.0.0.1:1112" "127.0.0.1:1114"'
+
+ self.assertEqual(
+ [('127.0.0.1', 1112), ('127.0.0.1', 1114)],
+ self.controller.get_listeners(Listener.CONTROL)
+ )
+
+ self.assertEqual([1112, 1114], self.controller.get_ports(Listener.CONTROL))
+
+ # unix socket file
+
+ get_info_mock.return_value = '"unix:/tmp/tor/socket"'
+
+ self.assertEqual([], self.controller.get_listeners(Listener.CONTROL))
+ self.assertEqual([], self.controller.get_ports(Listener.CONTROL))
+
+ @patch('stem.control.Controller.get_info')
+ @patch('stem.control.Controller.get_conf')
def test_get_socks_listeners_old(self, get_conf_mock, get_info_mock):
"""
Exercises the get_socks_listeners() method as though talking to an old tor