[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[tor-commits] [stem/master] Implement Controller.map_address
commit 8e54f37f64ea04a0772d825276f4e5e4a34d4df2
Author: Ravi Chandra Padmala <neenaoffline@xxxxxxxxx>
Date: Wed Sep 5 18:29:10 2012 +0530
Implement Controller.map_address
---
run_tests.py | 2 +
stem/control.py | 25 +++++++++++
stem/response/__init__.py | 16 +++++--
stem/response/mapaddress.py | 38 +++++++++++++++++
test/__init__.py | 1 +
test/integ/control/controller.py | 22 ++++++++++
test/runner.py | 2 +-
test/settings.cfg | 1 +
test/unit/response/mapaddress.py | 82 ++++++++++++++++++++++++++++++++++++++
test/utils.py | 61 ++++++++++++++++++++++++++++
10 files changed, 244 insertions(+), 6 deletions(-)
diff --git a/run_tests.py b/run_tests.py
index 4ad551a..b6ee64a 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -27,6 +27,7 @@ import test.unit.response.getconf
import test.unit.response.protocolinfo
import test.unit.response.authchallenge
import test.unit.response.singleline
+import test.unit.response.mapaddress
import test.unit.util.conf
import test.unit.util.connection
import test.unit.util.enum
@@ -120,6 +121,7 @@ UNIT_TESTS = (
test.unit.response.getinfo.TestGetInfoResponse,
test.unit.response.getconf.TestGetConfResponse,
test.unit.response.singleline.TestSingleLineResponse,
+ test.unit.response.mapaddress.TestMapAddressResponse,
test.unit.response.protocolinfo.TestProtocolInfoResponse,
test.unit.response.authchallenge.TestAuthChallengeResponse,
test.unit.connection.authentication.TestAuthenticate,
diff --git a/stem/control.py b/stem/control.py
index a9df1ba..8b407cb 100644
--- a/stem/control.py
+++ b/stem/control.py
@@ -30,6 +30,7 @@ interacting at a higher level.
|- new_circuit - create new circuits
|- extend_circuit - create new circuits and extend existing ones
|- repurpose_circuit - change a circuit's purpose
+ |- map_address - maps one address to another such that connections to the original are replaced with the other
|- get_version - convenience method to get tor version
|- authenticate - convenience method to authenticate the controller
+- protocolinfo - convenience method to get the protocol info
@@ -1127,6 +1128,30 @@ class Controller(BaseController):
raise stem.socket.ProtocolError("EXTENDCIRCUIT returned unexpected response code: %s" % response.code)
return int(new_circuit)
+
+ def map_address(self, mapping):
+ """
+ Map addresses to replacement addresses. Tor replaces subseqent connections
+ to the original addresses with the replacement addresses.
+
+ If the original address is a null address, i.e., one of "0.0.0.0", "::0", or
+ "." Tor picks an original address itself and returns it in the reply. If the
+ original address is already mapped to a different address the mapping is
+ removed.
+
+ :param dict mapping: mapping of original addresses to replacement addresses
+
+ :raises:
+ * :class:`stem.socket.InvalidRequest` if the addresses are malformed
+ * :class:`stem.socket.OperationFailed` if Tor couldn't fulfill the request
+
+ :returns: dictionary with original -> replacement address mappings
+ """
+
+ response = self.msg("MAPADDRESS %s" % " ".join([k + "=" + mapping[k] for k in mapping.iterkeys()]))
+ stem.response.convert("MAPADDRESS", response)
+
+ return response.entries
def _case_insensitive_lookup(entries, key, default = UNDEFINED):
"""
diff --git a/stem/response/__init__.py b/stem/response/__init__.py
index 14a0280..4a05be2 100644
--- a/stem/response/__init__.py
+++ b/stem/response/__init__.py
@@ -60,11 +60,14 @@ def convert(response_type, message, **kwargs):
* **\*** GETINFO
* **\*** GETCONF
+ * **&** **^** MAPADDRESS
* PROTOCOLINFO
* AUTHCHALLENGE
* SINGLELINE
**\*** can raise a :class:`stem.socket.InvalidArguments` exception
+ **^** can raise a :class:`stem.socket.InvalidRequest` exception
+ **&** can raise a :class:`stem.socket.OperationFailed` exception
:param str response_type: type of tor response to convert to
:param stem.response.ControlMessage message: message to be converted
@@ -73,6 +76,7 @@ def convert(response_type, message, **kwargs):
:raises:
* :class:`stem.socket.ProtocolError` the message isn't a proper response of that type
* :class:`stem.socket.InvalidArguments` the arguments given as input are invalid
+ * :class:`stem.socket.InvalidRequest` the arguments given as input are invalid
* TypeError if argument isn't a :class:`stem.response.ControlMessage` or response_type isn't supported
"""
@@ -80,6 +84,7 @@ def convert(response_type, message, **kwargs):
import stem.response.getconf
import stem.response.protocolinfo
import stem.response.authchallenge
+ import stem.response.mapaddress
if not isinstance(message, ControlMessage):
raise TypeError("Only able to convert stem.response.ControlMessage instances")
@@ -87,10 +92,11 @@ def convert(response_type, message, **kwargs):
response_types = {
"GETINFO": stem.response.getinfo.GetInfoResponse,
"GETCONF": stem.response.getconf.GetConfResponse,
+ "MAPADDRESS": stem.response.mapaddress.MapAddressResponse,
"SINGLELINE": SingleLineResponse,
"PROTOCOLINFO": stem.response.protocolinfo.ProtocolInfoResponse,
"AUTHCHALLENGE": stem.response.authchallenge.AuthChallengeResponse,
- }
+ }
try:
response_class = response_types[response_type]
@@ -116,15 +122,15 @@ class ControlMessage(object):
def is_ok(self):
"""
- Checks if all of our lines have a 250 response.
+ Checks if any of our lines have a 250 response.
- :returns: True if all lines have a 250 response code, False otherwise
+ :returns: True if any lines have a 250 response code, False otherwise
"""
for code, _, _ in self._parsed_content:
- if code != "250": return False
+ if code == "250": return True
- return True
+ return False
def content(self):
"""
diff --git a/stem/response/mapaddress.py b/stem/response/mapaddress.py
new file mode 100644
index 0000000..2d1a498
--- /dev/null
+++ b/stem/response/mapaddress.py
@@ -0,0 +1,38 @@
+import stem.socket
+import stem.response
+
+class MapAddressResponse(stem.response.ControlMessage):
+ """
+ Reply for a MAPADDRESS query.
+ Doesn't raise an exception unless no addresses were mapped successfully.
+
+ :var dict entries: mapping between the original and replacement addresses
+
+ :raises:
+ * :class:`stem.socket.OperationFailed` if Tor was unable to satisfy the request
+ * :class:`stem.socket.InvalidRequest` if the addresses provided were invalid
+ """
+
+ def _parse_message(self):
+ # Example:
+ # 250-127.192.10.10=torproject.org
+ # 250 1.2.3.4=tor.freehaven.net
+
+ if not self.is_ok():
+ for code, _, message in self.content():
+ if code == "512":
+ raise stem.socket.InvalidRequest(code, message)
+ elif code == "451":
+ raise stem.socket.OperationFailed(code, message)
+ else:
+ raise stem.socket.ProtocolError("MAPADDRESS returned unexpected response code: %s", code)
+
+ self.entries = {}
+
+ for code, _, message in self.content():
+ if code == "250":
+ try: key, value = message.split("=", 1)
+ except ValueError: raise stem.socket.ProtocolError(None, "Not a mapping")
+
+ self.entries[key] = value
+
diff --git a/test/__init__.py b/test/__init__.py
index 78d6543..e46822c 100644
--- a/test/__init__.py
+++ b/test/__init__.py
@@ -8,5 +8,6 @@ __all__ = [
"output",
"prompt",
"runner",
+ "utils",
]
diff --git a/test/integ/control/controller.py b/test/integ/control/controller.py
index 5f5113c..f8fd56e 100644
--- a/test/integ/control/controller.py
+++ b/test/integ/control/controller.py
@@ -6,6 +6,7 @@ from __future__ import with_statement
import re
import shutil
+import socket
import unittest
import tempfile
@@ -14,6 +15,7 @@ import stem.socket
import stem.version
import stem.response.protocolinfo
import test.runner
+import test.utils
class TestController(unittest.TestCase):
def test_from_port(self):
@@ -352,6 +354,8 @@ class TestController(unittest.TestCase):
Test controller.signal with valid and invalid signals.
"""
+ if test.runner.require_control(self): return
+
with test.runner.get_runner().get_tor_controller() as controller:
# valid signal
controller.signal("CLEARDNSCACHE")
@@ -401,4 +405,22 @@ class TestController(unittest.TestCase):
self.assertRaises(stem.socket.InvalidRequest, controller.repurpose_circuit, 'f934h9f3h4', "fooo")
self.assertRaises(stem.socket.InvalidRequest, controller.repurpose_circuit, '4', "fooo")
+
+ def test_mapaddress(self):
+
+ if test.runner.require_control(self): return
+
+ runner = test.runner.get_runner()
+
+ with runner.get_tor_controller() as controller:
+ controller.map_address({'1.2.1.2': 'ifconfig.me'})
+
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect(('127.0.0.1', int(controller.get_conf('SocksPort'))))
+ test.utils.negotiate_socks(s, '1.2.1.2', 80)
+ s.sendall(test.utils.ip_request)
+ response = s.recv(1000)
+ ip_addr = response[response.find("\r\n\r\n"):].strip()
+
+ socket.inet_aton(ip_addr) # validate IP
diff --git a/test/runner.py b/test/runner.py
index cc8cf7b..b748bfe 100644
--- a/test/runner.py
+++ b/test/runner.py
@@ -69,7 +69,7 @@ ERROR_ATTR = (term.Color.RED, term.Attr.BOLD)
BASE_TORRC = """# configuration for stem integration tests
DataDirectory %s
-SocksPort 0
+SocksPort 29327
DownloadExtraInfo 1
"""
diff --git a/test/settings.cfg b/test/settings.cfg
index 44e28ab..75d5078 100644
--- a/test/settings.cfg
+++ b/test/settings.cfg
@@ -76,6 +76,7 @@ target.prereq RUN_PTRACE => TORRC_DISABLE_DEBUGGER_ATTACHMENT
# means that each of these targets will have a dedicated integration test run.
target.torrc RUN_NONE =>
+target.torrc ONLINE => PORT
target.torrc RUN_OPEN => PORT
target.torrc RUN_PASSWORD => PORT, PASSWORD
target.torrc RUN_COOKIE => PORT, COOKIE
diff --git a/test/unit/response/mapaddress.py b/test/unit/response/mapaddress.py
new file mode 100644
index 0000000..b8045c1
--- /dev/null
+++ b/test/unit/response/mapaddress.py
@@ -0,0 +1,82 @@
+"""
+Unit tests for the stem.response.mapaddress.MapAddressResponse class.
+"""
+
+import unittest
+
+import stem.socket
+import stem.response
+import stem.response.mapaddress
+import test.mocking as mocking
+
+SINGLE_RESPONSE = """250 foo=bar"""
+
+BATCH_RESPONSE = """\
+250-foo=bar
+250-baz=quux
+250-gzzz=bzz
+250 120.23.23.2=torproject.org"""
+
+INVALID_EMPTY_RESPONSE = "250 OK"
+INVALID_RESPONSE = "250 foo is bar"
+
+PARTIAL_FAILURE_RESPONSE = """512-syntax error: mapping '2389' is not of expected form 'foo=bar'
+512-syntax error: mapping '23' is not of expected form 'foo=bar'.
+250 23=324"""
+
+UNRECOGNIZED_KEYS_RESPONSE = "512 syntax error: mapping '2389' is not of expected form 'foo=bar'"
+
+FAILED_RESPONSE = "451 Resource exhausted"
+
+class TestMapAddressResponse(unittest.TestCase):
+ def test_single_response(self):
+ """
+ Parses a MAPADDRESS reply response with a single address mapping.
+ """
+
+ control_message = mocking.get_message(SINGLE_RESPONSE)
+ stem.response.convert("MAPADDRESS", control_message)
+ self.assertEqual({"foo": "bar"}, control_message.entries)
+
+ def test_batch_response(self):
+ """
+ Parses a MAPADDRESS reply with multiple address mappings
+ """
+
+ control_message = mocking.get_message(BATCH_RESPONSE)
+ stem.response.convert("MAPADDRESS", control_message)
+
+ expected = {
+ "foo": "bar",
+ "baz": "quux",
+ "gzzz": "bzz",
+ "120.23.23.2": "torproject.org"
+ }
+
+ self.assertEqual(expected, control_message.entries)
+
+ def test_invalid_requests(self):
+ """
+ Parses a MAPADDRESS replies that contain an error code due to hostname syntax errors.
+ """
+
+ control_message = mocking.get_message(UNRECOGNIZED_KEYS_RESPONSE)
+ self.assertRaises(stem.socket.InvalidRequest, stem.response.convert, "MAPADDRESS", control_message)
+ control_message = mocking.get_message(UNRECOGNIZED_KEYS_RESPONSE)
+ expected = { "23": "324" }
+ control_message = mocking.get_message(PARTIAL_FAILURE_RESPONSE)
+ stem.response.convert("MAPADDRESS", control_message)
+ self.assertEqual(expected, control_message.entries)
+
+ def test_invalid_response(self):
+ """
+ Parses a malformed MAPADDRESS reply that contains an invalid response code.
+ This is a proper controller message, but malformed according to the
+ MAPADDRESS's spec.
+ """
+
+ control_message = mocking.get_message(INVALID_EMPTY_RESPONSE)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "MAPADDRESS", control_message)
+ control_message = mocking.get_message(INVALID_RESPONSE)
+ self.assertRaises(stem.socket.ProtocolError, stem.response.convert, "MAPADDRESS", control_message)
+
diff --git a/test/utils.py b/test/utils.py
new file mode 100644
index 0000000..8ed9297
--- /dev/null
+++ b/test/utils.py
@@ -0,0 +1,61 @@
+import struct
+import socket
+
+from stem.socket import ProtocolError
+import test.runner
+
+error_msgs = {
+ 0x5a: "SOCKS4A request granted",
+ 0x5b: "SOCKS4A request rejected or failed",
+ 0x5c: "SOCKS4A request failed because client is not running identd (or not reachable from the server)",
+ 0x5d: "SOCKS4A request failed because client's identd could not confirm the user ID string in the request",
+}
+
+ip_request = """GET /ip HTTP/1.0
+Host: ifconfig.me
+Accept-Encoding: identity
+
+"""
+
+def external_ip(sock):
+ """
+ Returns the externally visible IP address when using a SOCKS4a proxy.
+
+ :param socket sock: socket connected to a SOCKS4a proxy server
+
+ :returns: externally visible IP address, or None if it isn't able to
+ """
+
+ try:
+ negotiate_socks(sock, "ifconfig.me", 80)
+ s.sendall(req)
+ response = s.recv(1000)
+
+ return response[response.find("\n\n"):].strip()
+ except:
+ pass
+
+def negotiate_socks(sock, host, port):
+ """
+ Negotiate with a socks4a server. Closes the socket and raises an exception on
+ failure.
+
+ :param socket sock: socket connected to socks4a server
+ :param str host: host to connect to
+ :param int port: port to connect to
+
+ :raises: :class:`stem.socket.ProtocolError` if the socks server doesn't grant our request
+
+ :returns: a list with the IP address and the port that the proxy connected to
+ """
+
+ request = "\x04\x01" + struct.pack("!H", port) + "\x00\x00\x00\x01" + "\x00" + host + "\x00"
+ sock.sendall(request)
+ response = sock.recv(8)
+
+ if len(response) != 8 or response[0] != "\x00" or response[1] != "\x5a":
+ sock.close()
+ raise ProtocolError(error_msgs.get(response[1], "SOCKS server returned unrecognized error code"))
+
+ return [socket.inet_ntoa(response[4:]), struct.unpack("!H", response[2:4])[0]]
+
_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits