commit 8e54f37f64ea04a0772d825276f4e5e4a34d4df2 Author: Ravi Chandra Padmala neenaoffline@gmail.com 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@lists.torproject.org