From f289084c83190cc72db4a70c58f007ec62e75247 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 7 Feb 2019 08:22:45 -0500 Subject: [PATCH] bpo-24209: In http.server script, rely on getaddrinfo to bind to preferred address based on the bind parameter. (#11767) In http.server script, rely on getaddrinfo to bind to preferred address based on the bind parameter. As a result, now IPv6 is used as the default (including IPv4 on dual-stack systems). Enhanced tests. --- Lib/http/server.py | 30 ++++++--- Lib/test/test_httpservers.py | 66 +++++++++++++++---- .../2019-02-06-01-40-55.bpo-24209.awtwPD.rst | 1 + 3 files changed, 75 insertions(+), 22 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2019-02-06-01-40-55.bpo-24209.awtwPD.rst diff --git a/Lib/http/server.py b/Lib/http/server.py index 29c720ea7ea..b247675ec45 100644 --- a/Lib/http/server.py +++ b/Lib/http/server.py @@ -1224,24 +1224,34 @@ class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): self.log_message("CGI script exited OK") +def _get_best_family(*address): + infos = socket.getaddrinfo( + *address, + type=socket.SOCK_STREAM, + flags=socket.AI_PASSIVE, + ) + family, type, proto, canonname, sockaddr = next(iter(infos)) + return family, sockaddr + + def test(HandlerClass=BaseHTTPRequestHandler, ServerClass=ThreadingHTTPServer, - protocol="HTTP/1.0", port=8000, bind=""): + protocol="HTTP/1.0", port=8000, bind=None): """Test the HTTP request handler class. This runs an HTTP server on port 8000 (or the port argument). """ - server_address = (bind, port) - - if ':' in bind: - ServerClass.address_family = socket.AF_INET6 + ServerClass.address_family, addr = _get_best_family(bind, port) HandlerClass.protocol_version = protocol - with ServerClass(server_address, HandlerClass) as httpd: - sa = httpd.socket.getsockname() - serve_message = "Serving HTTP on {host} port {port} (http://{host}:{port}/) ..." - print(serve_message.format(host=sa[0], port=sa[1])) + with ServerClass(addr, HandlerClass) as httpd: + host, port = httpd.socket.getsockname()[:2] + url_host = f'[{host}]' if ':' in host else host + print( + f"Serving HTTP on {host} port {port} " + f"(http://{url_host}:{port}/) ..." + ) try: httpd.serve_forever() except KeyboardInterrupt: @@ -1254,7 +1264,7 @@ if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('--cgi', action='store_true', help='Run as CGI Server') - parser.add_argument('--bind', '-b', default='', metavar='ADDRESS', + parser.add_argument('--bind', '-b', metavar='ADDRESS', help='Specify alternate bind address ' '[default: all interfaces]') parser.add_argument('--directory', '-d', default=os.getcwd(), diff --git a/Lib/test/test_httpservers.py b/Lib/test/test_httpservers.py index 3d8e0af8b45..8357ee9145d 100644 --- a/Lib/test/test_httpservers.py +++ b/Lib/test/test_httpservers.py @@ -1118,21 +1118,63 @@ class MiscTestCase(unittest.TestCase): class ScriptTestCase(unittest.TestCase): + + def mock_server_class(self): + return mock.MagicMock( + return_value=mock.MagicMock( + __enter__=mock.MagicMock( + return_value=mock.MagicMock( + socket=mock.MagicMock( + getsockname=lambda: ('', 0), + ), + ), + ), + ), + ) + + @mock.patch('builtins.print') + def test_server_test_unspec(self, _): + mock_server = self.mock_server_class() + server.test(ServerClass=mock_server, bind=None) + self.assertIn( + mock_server.address_family, + (socket.AF_INET6, socket.AF_INET), + ) + + @mock.patch('builtins.print') + def test_server_test_localhost(self, _): + mock_server = self.mock_server_class() + server.test(ServerClass=mock_server, bind="localhost") + self.assertIn( + mock_server.address_family, + (socket.AF_INET6, socket.AF_INET), + ) + + ipv6_addrs = ( + "::", + "2001:0db8:85a3:0000:0000:8a2e:0370:7334", + "::1", + ) + + ipv4_addrs = ( + "0.0.0.0", + "8.8.8.8", + "127.0.0.1", + ) + @mock.patch('builtins.print') def test_server_test_ipv6(self, _): - mock_server = mock.MagicMock() - server.test(ServerClass=mock_server, bind="::") - self.assertEqual(mock_server.address_family, socket.AF_INET6) + for bind in self.ipv6_addrs: + mock_server = self.mock_server_class() + server.test(ServerClass=mock_server, bind=bind) + self.assertEqual(mock_server.address_family, socket.AF_INET6) - mock_server.reset_mock() - server.test(ServerClass=mock_server, - bind="2001:0db8:85a3:0000:0000:8a2e:0370:7334") - self.assertEqual(mock_server.address_family, socket.AF_INET6) - - mock_server.reset_mock() - server.test(ServerClass=mock_server, - bind="::1") - self.assertEqual(mock_server.address_family, socket.AF_INET6) + @mock.patch('builtins.print') + def test_server_test_ipv4(self, _): + for bind in self.ipv4_addrs: + mock_server = self.mock_server_class() + server.test(ServerClass=mock_server, bind=bind) + self.assertEqual(mock_server.address_family, socket.AF_INET) def test_main(verbose=None): diff --git a/Misc/NEWS.d/next/Library/2019-02-06-01-40-55.bpo-24209.awtwPD.rst b/Misc/NEWS.d/next/Library/2019-02-06-01-40-55.bpo-24209.awtwPD.rst new file mode 100644 index 00000000000..4d555fd4125 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-02-06-01-40-55.bpo-24209.awtwPD.rst @@ -0,0 +1 @@ +In http.server script, rely on getaddrinfo to bind to preferred address based on the bind parameter. Now default bind or binding to a name may bind to IPv6 or dual-stack, depending on the environment. \ No newline at end of file