[tor-commits] [stem/master] Implement Controller.map_address

atagar at torproject.org atagar at torproject.org
Mon Sep 24 16:03:45 UTC 2012


commit 8e54f37f64ea04a0772d825276f4e5e4a34d4df2
Author: Ravi Chandra Padmala <neenaoffline at 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]]
+





More information about the tor-commits mailing list