]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
Add support for source IP address to SimpleAsyncHTTPClient. 2528/head
authorGasper Zejn <zejn@kiberpipa.org>
Fri, 2 Nov 2018 12:38:22 +0000 (13:38 +0100)
committerGasper Zejn <zejn@kiberpipa.org>
Fri, 2 Nov 2018 12:38:22 +0000 (13:38 +0100)
Extend support for HTTPRequest's network_interface parameter to
SimpleAsyncHTTPClient. This enables binding to specific local IP and
enables connecting to WebSockets via a specific IP address.

To use it you create a HTTPRequest with network_interface parameter set
to local IP address to be used as source IP. websocket_connect then
passes the request to SimpleAsyncHTTPClient.

Note that even though the parameter name is `network_interface`, the
CurlAsyncHTTPClient may support (OS dependant) passing in interface
name, resolvable hostname or IP address, while this patch only adds
IP address support to SimpleAsyncHTTPClient.

tornado/httpclient.py
tornado/simple_httpclient.py
tornado/test/httpclient_test.py

index e2623d5328457f506b70761a7f4f81a2aec295b1..d1c92a49f1fce27d30cd3e70b1ab1a37836ff699 100644 (file)
@@ -422,8 +422,8 @@ class HTTPRequest(object):
            New in Tornado 4.0.
         :arg bool use_gzip: Deprecated alias for ``decompress_response``
            since Tornado 4.0.
-        :arg str network_interface: Network interface to use for request.
-           ``curl_httpclient`` only; see note below.
+        :arg str network_interface: Network interface or source IP to use for request.
+           See ``curl_httpclient`` note below.
         :arg collections.abc.Callable streaming_callback: If set, ``streaming_callback`` will
            be run with each chunk of data as it is received, and
            ``HTTPResponse.body`` and ``HTTPResponse.buffer`` will be empty in
index d6ac20f91237e13589efcbe0f448b98879fdc063..81a0fd24760e4446bce78135531b95848ffb6f21 100644 (file)
@@ -12,7 +12,12 @@ from tornado import httputil
 from tornado.http1connection import HTTP1Connection, HTTP1ConnectionParameters
 from tornado.ioloop import IOLoop
 from tornado.iostream import StreamClosedError, IOStream
-from tornado.netutil import Resolver, OverrideResolver, _client_ssl_defaults
+from tornado.netutil import (
+    Resolver,
+    OverrideResolver,
+    _client_ssl_defaults,
+    is_valid_ip,
+)
 from tornado.log import gen_log
 from tornado.tcpclient import TCPClient
 
@@ -306,6 +311,16 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
 
             ssl_options = self._get_ssl_options(self.parsed.scheme)
 
+            source_ip = None
+            if self.request.network_interface:
+                if is_valid_ip(self.request.network_interface):
+                    source_ip = self.request.network_interface
+                else:
+                    raise ValueError(
+                        "Unrecognized IPv4 or IPv6 address for network_interface, got %r"
+                        % (self.request.network_interface,)
+                    )
+
             timeout = min(self.request.connect_timeout, self.request.request_timeout)
             if timeout:
                 self._timeout = self.io_loop.add_timeout(
@@ -318,6 +333,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
                     af=af,
                     ssl_options=ssl_options,
                     max_buffer_size=self.max_buffer_size,
+                    source_ip=source_ip,
                 )
 
                 if self.final_callback is None:
@@ -340,7 +356,6 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
                 ):
                     raise KeyError("unknown method %s" % self.request.method)
                 for key in (
-                    "network_interface",
                     "proxy_host",
                     "proxy_port",
                     "proxy_username",
index c30c1b56617b2ec13114fe892cc5c055e1dbaa2a..0a732ddc3a1249a8d87e40c677f77895da328bb6 100644 (file)
@@ -467,6 +467,18 @@ X-XSS-Protection: 1;
         response2 = yield self.http_client.fetch(response.request)
         self.assertEqual(response2.body, b"Hello world!")
 
+    @gen_test
+    def test_bind_source_ip(self):
+        url = self.get_url("/hello")
+        request = HTTPRequest(url, network_interface="127.0.0.1")
+        response = yield self.http_client.fetch(request)
+        self.assertEqual(response.code, 200)
+
+        with self.assertRaises((ValueError, HTTPError)) as context:
+            request = HTTPRequest(url, network_interface="not-interface-or-ip")
+            yield self.http_client.fetch(request)
+        self.assertIn("not-interface-or-ip", str(context.exception))
+
     def test_all_methods(self):
         for method in ["GET", "DELETE", "OPTIONS"]:
             response = self.fetch("/all_methods", method=method)