From 9ba61c636899ed0f7472fb2f4975a53720d1b0ff Mon Sep 17 00:00:00 2001 From: Gasper Zejn Date: Fri, 2 Nov 2018 13:38:22 +0100 Subject: [PATCH] Add support for source IP address to SimpleAsyncHTTPClient. 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 | 4 ++-- tornado/simple_httpclient.py | 19 +++++++++++++++++-- tornado/test/httpclient_test.py | 12 ++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/tornado/httpclient.py b/tornado/httpclient.py index e2623d532..d1c92a49f 100644 --- a/tornado/httpclient.py +++ b/tornado/httpclient.py @@ -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 diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index d6ac20f91..81a0fd247 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -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", diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py index c30c1b566..0a732ddc3 100644 --- a/tornado/test/httpclient_test.py +++ b/tornado/test/httpclient_test.py @@ -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) -- 2.47.2