From ffa07b341636952b822a9d3e22d181f3e9b0952b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 17:54:02 +0100 Subject: [PATCH 01/43] add socket.bind_socket() function + tests --- Doc/library/socket.rst | 16 +++++- Doc/whatsnew/3.8.rst | 6 +++ Lib/socket.py | 66 +++++++++++++++++++++++ Lib/test/test_socket.py | 112 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 198 insertions(+), 2 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index d466884d613516..07b8ccc0f308ee 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -575,7 +575,7 @@ The following functions all create :ref:`socket objects `. .. function:: create_connection(address[, timeout[, source_address]]) - Connect to a TCP service listening on the Internet *address* (a 2-tuple + Connect to a TCP service listening on the internet *address* (a 2-tuple ``(host, port)``), and return the socket object. This is a higher-level function than :meth:`socket.connect`: if *host* is a non-numeric hostname, it will try to resolve it for both :data:`AF_INET` and :data:`AF_INET6`, @@ -595,6 +595,20 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. +.. function:: bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, + backlog=100, reuse_port=False): + + Convenience function which creates a socket bound to *address* (a 2-tuple + ``(host, port)``) and return the socket object upon which you can call + :meth:`socket.accept()` in order to accept new connections. + If *host* is an empty string or ``None`` all network interfaces are assumed. + If *family* is :data:`AF_UNSPEC` or ``None`` the address family will be + determined from the *host* specified in *address*. + *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. + *backlog* is the queue size passed to :meth:`socket.listen` if + :data:`SOCK_STREAM` *type* is used. + + .. versionadded:: 3.8 .. function:: fromfd(fd, family, type, proto=0) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index a90bc274eb6bfa..660b0548213373 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -213,6 +213,12 @@ pathlib contain characters unrepresentable at the OS level. (Contributed by Serhiy Storchaka in :issue:`33721`.) +socket +------ + +Added :meth:`~socket.bind_socket()` a convenience function. +(Contributed by Giampaolo Rodola in :issue:`17561`.) + shutil ------ diff --git a/Lib/socket.py b/Lib/socket.py index 772b9e185bf1c6..9649253d61762e 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -728,6 +728,72 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, else: raise error("getaddrinfo returns an empty list") +def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=100, + reuse_port=False): + """Convenience function which creates a socket bound to *address* + (a 2-tuple (host, port)) and return the socket object. + + If *host* is an empty string or None all network interfaces are assumed. + + If *family* is AF_UNSPEC or None the address family will be + determined from the *host* specified in *address*. + + *type* should be either SOCK_STREAM or SOCK_DGRAM. + + *backlog* is the queue size passed to socket.listen() and is ignored + for SOCK_DGRAM socket types. + + >>> with bind_socket((None, 8000)) as server: + ... while True: + ... conn, addr = server.accept() + ... # handle new connection + """ + host, port = address + if host == "": + host = None # all interfaces + if not family: + family = AF_UNSPEC + reuse_addr = hasattr(_socket, 'SO_REUSEADDR') and \ + os.name == 'posix' and sys.platform != 'cygwin' + info = getaddrinfo(host, port, family, type, 0, AI_PASSIVE) + # prefer AF_INET over AF_INET6 + if family == AF_UNSPEC: + info.sort(key=lambda x: x[0] == AF_INET, reverse=True) + err = None + for res in info: + af, socktype, proto, canonname, sa = res + try: + sock = socket(af, socktype, proto) + except error as _: + err = _ + if err.errno == errno.EAFNOSUPPORT: + continue + else: + raise + try: + if reuse_addr: + try: + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + except error: + pass + if reuse_port: + sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) + sock.bind(sa) + if socktype == SOCK_STREAM: + sock.listen(backlog) + # Break explicitly a reference cycle. + err = None + return sock + except error: + sock.close() + raise + + if err is not None: + raise err + else: + raise error("getaddrinfo returns an empty list") + + def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): """Resolve host and port into list of address info entries. diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 7c5167d85033cf..f74d64d0cfa70b 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6053,9 +6053,119 @@ def test_new_tcp_flags(self): self.assertEqual([], unknown, "New TCP flags were discovered. See bpo-32394 for more information") + +class BindSocketTest(unittest.TestCase): + + def test_address(self): + port = support.find_unused_port() + with socket.bind_socket(("127.0.0.1", port)) as sock: + self.assertEqual(sock.getsockname()[0], "127.0.0.1") + self.assertEqual(sock.getsockname()[1], port) + + def test_family(self): + # determined by address + with socket.bind_socket(("127.0.0.1", 0)) as sock: + self.assertEqual(sock.family, socket.AF_INET) + if support.IPV6_ENABLED: + with socket.bind_socket(("::1", 0)) as sock: + self.assertEqual(sock.family, socket.AF_INET6) + # determined by 'family' arg + with socket.bind_socket(("localhost", 0), + family=socket.AF_INET) as sock: + self.assertEqual(sock.family, socket.AF_INET) + if support.IPV6_ENABLED: + with socket.bind_socket(("localhost", 0), + family=socket.AF_INET6) as sock: + self.assertEqual(sock.family, socket.AF_INET6) + + def test_type(self): + with socket.bind_socket((None, 0)) as sock: + self.assertEqual(sock.type, socket.SOCK_STREAM) + with socket.bind_socket((None, 0), type=socket.SOCK_DGRAM) as sock: + self.assertEqual(sock.type, socket.SOCK_DGRAM) + + @unittest.skipIf(not hasattr(socket, "SO_REUSEPORT"), + "SO_REUSEPORT not supported") + def test_reuse_port(self): + with socket.bind_socket(("127.0.0.1", 0)) as sock: + opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) + self.assertEqual(opt, 0) + with socket.bind_socket(("127.0.0.1", 0), reuse_port=True) as sock: + opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) + self.assertEqual(opt, 1) + + +class BindSocketFunctionalTest(unittest.TestCase): + timeout = 3 + + def setUp(self): + self.thread = None + + def tearDown(self): + if self.thread is not None: + self.thread.join(self.timeout) + + def echo_server(self, sock): + def run(): + if sock.type == socket.SOCK_STREAM: + conn, _ = sock.accept() + conn.settimeout(self.timeout) + event.wait(self.timeout) + with conn: + msg = conn.recv(1024) + if not msg: + return + conn.sendall(msg) + else: + msg, addr = sock.recvfrom(1024) + sock.sendto(msg, addr) + + event = threading.Event() + sock.settimeout(self.timeout) + self.thread = threading.Thread(target=run) + self.thread.start() + event.set() + + def echo_test(self, sock): + self.echo_server(sock) + server_addr = sock.getsockname()[:2] + if sock.type == socket.SOCK_STREAM: + with socket.create_connection(server_addr) as client: + client.sendall(b'foo') + self.assertEqual(client.recv(1024), b'foo') + else: + with socket.socket(sock.family, sock.type) as client: + client.sendto(b'foo', server_addr) + msg, _ = client.recvfrom(1024) + self.assertEqual(msg, b'foo') + + def test_tcp4(self): + with socket.bind_socket(("localhost", 0), + socket.AF_INET, socket.SOCK_STREAM) as sock: + self.echo_test(sock) + + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') + def test_tcp6(self): + with socket.bind_socket(("localhost", 0), + socket.AF_INET6, socket.SOCK_STREAM) as sock: + self.echo_test(sock) + + def test_udp4(self): + with socket.bind_socket(("localhost", 0), + socket.AF_INET, socket.SOCK_DGRAM) as sock: + self.echo_test(sock) + + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') + def test_udp6(self): + with socket.bind_socket(("localhost", 0), + socket.AF_INET6, socket.SOCK_DGRAM) as sock: + self.echo_test(sock) + + def test_main(): tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest, - TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, UDPTimeoutTest ] + TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, + UDPTimeoutTest, BindSocketTest, BindSocketFunctionalTest] tests.extend([ NonBlockingTCPTests, From 5618b21e09c15292fae07c8f3cd3f8b2e59d9529 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 18:14:03 +0100 Subject: [PATCH 02/43] make ftplib use bind_socket() (reuse code) --- Lib/ftplib.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/Lib/ftplib.py b/Lib/ftplib.py index 9611282ecacb25..1691c20e3fe75f 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -302,26 +302,7 @@ def sendeprt(self, host, port): def makeport(self): '''Create a new socket and send a PORT command for it.''' - err = None - sock = None - for res in socket.getaddrinfo(None, 0, self.af, socket.SOCK_STREAM, 0, socket.AI_PASSIVE): - af, socktype, proto, canonname, sa = res - try: - sock = socket.socket(af, socktype, proto) - sock.bind(sa) - except OSError as _: - err = _ - if sock: - sock.close() - sock = None - continue - break - if sock is None: - if err is not None: - raise err - else: - raise OSError("getaddrinfo returns an empty list") - sock.listen(1) + sock = socket.bind_socket((None, 0), family=self.af, backlog=1) port = sock.getsockname()[1] # Get proper port host = self.sock.getsockname()[0] # Get proper host if self.af == socket.AF_INET: From 6b3e6341189b4051f4f79912c9cf6e0b05ed83e8 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 18:52:45 +0100 Subject: [PATCH 03/43] use bind_socket() in tests (reuse code) --- Lib/idlelib/rpc.py | 5 ++--- Lib/test/_test_multiprocessing.py | 8 ++------ Lib/test/test_asyncio/functional.py | 10 +--------- Lib/test/test_asyncio/test_base_events.py | 3 +-- Lib/test/test_asyncio/test_events.py | 12 +++--------- Lib/test/test_asyncio/test_streams.py | 11 +++-------- Lib/test/test_httplib.py | 5 +---- Lib/test/test_ssl.py | 4 +--- Lib/test/test_support.py | 6 ++---- 9 files changed, 16 insertions(+), 48 deletions(-) diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index 9962477cc56185..b9ed4c315642b7 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -530,9 +530,8 @@ class RPCClient(SocketIO): nextseq = 1 # Requests coming from the client are odd numbered def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM): - self.listening_sock = socket.socket(family, type) - self.listening_sock.bind(address) - self.listening_sock.listen(1) + self.listening_sock = socket.bind_socket( + family=family, type=type, backlog=1) def accept(self): working_sock, address = self.listening_sock.accept() diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index 2f839b952126a3..aebcd81c2391ba 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -3323,9 +3323,7 @@ def _listener(cls, conn, families): new_conn.close() l.close() - l = socket.socket() - l.bind((test.support.HOST, 0)) - l.listen() + l = socket.bind_socket((test.support.HOST, 0)) conn.send(l.getsockname()) new_conn, addr = l.accept() conn.send(new_conn) @@ -4077,9 +4075,7 @@ def _child_test_wait_socket(cls, address, slow): def test_wait_socket(self, slow=False): from multiprocessing.connection import wait - l = socket.socket() - l.bind((test.support.HOST, 0)) - l.listen() + l = socket.bind_socket((test.support.HOST, 0)) addr = l.getsockname() readers = [] procs = [] diff --git a/Lib/test/test_asyncio/functional.py b/Lib/test/test_asyncio/functional.py index 6b5b3cc907cc04..c79187a34b84d9 100644 --- a/Lib/test/test_asyncio/functional.py +++ b/Lib/test/test_asyncio/functional.py @@ -60,21 +60,13 @@ def tcp_server(self, server_prog, *, else: addr = ('127.0.0.1', 0) - sock = socket.socket(family, socket.SOCK_STREAM) - + sock = socket.bind_socket(family=family, backlog=backlog) if timeout is None: raise RuntimeError('timeout is required') if timeout <= 0: raise RuntimeError('only blocking sockets are supported') sock.settimeout(timeout) - try: - sock.bind(addr) - sock.listen(backlog) - except OSError as ex: - sock.close() - raise ex - return TestThreadedServer( self, sock, server_prog, timeout, max_clients) diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 53854758a27d4c..a615c5d6c4cfbf 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1641,8 +1641,7 @@ class Err(OSError): self.assertTrue(m_sock.close.called) def test_create_datagram_endpoint_sock(self): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.bind(('127.0.0.1', 0)) + sock = socket.bind_socket(('127.0.0.1', 0), type=socket.SOCK_DGRAM) fut = self.loop.create_datagram_endpoint( lambda: MyDatagramProto(create_future=True, loop=self.loop), sock=sock) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index a2b954eec4ad79..207e61cdf743ba 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -667,9 +667,7 @@ def data_received(self, data): super().data_received(data) self.transport.write(expected_response) - lsock = socket.socket() - lsock.bind(('127.0.0.1', 0)) - lsock.listen(1) + lsock = socket.bind_socket(('127.0.0.1', 0), backlog=1) addr = lsock.getsockname() message = b'test data' @@ -1118,9 +1116,7 @@ def connection_made(self, transport): super().connection_made(transport) proto.set_result(self) - sock_ob = socket.socket(type=socket.SOCK_STREAM) - sock_ob.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock_ob.bind(('0.0.0.0', 0)) + sock_ob = socket.bind_socket(('0.0.0.0', 0)) f = self.loop.create_server(TestMyProto, sock=sock_ob) server = self.loop.run_until_complete(f) @@ -1136,9 +1132,7 @@ def connection_made(self, transport): server.close() def test_create_server_addr_in_use(self): - sock_ob = socket.socket(type=socket.SOCK_STREAM) - sock_ob.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock_ob.bind(('0.0.0.0', 0)) + sock_ob = socket.bind_socket(('0.0.0.0', 0)) f = self.loop.create_server(MyProto, sock=sock_ob) server = self.loop.run_until_complete(f) diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index 043fac7c6a2d7e..f7846e4c843388 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -592,8 +592,7 @@ async def handle_client(self, client_reader, client_writer): await client_writer.wait_closed() def start(self): - sock = socket.socket() - sock.bind(('127.0.0.1', 0)) + sock = socket.bind_socket(('127.0.0.1', 0)) self.server = self.loop.run_until_complete( asyncio.start_server(self.handle_client, sock=sock, @@ -605,8 +604,7 @@ def handle_client_callback(self, client_reader, client_writer): client_writer)) def start_callback(self): - sock = socket.socket() - sock.bind(('127.0.0.1', 0)) + sock = socket.bind_socket(('127.0.0.1', 0)) addr = sock.getsockname() sock.close() self.server = self.loop.run_until_complete( @@ -796,10 +794,7 @@ def test_drain_raises(self): def server(): # Runs in a separate thread. - sock = socket.socket() - with sock: - sock.bind(('localhost', 0)) - sock.listen(1) + with socket.bind_socket(('localhost', 0)) as sock: addr = sock.getsockname() q.put(addr) clt, _ = sock.accept() diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index f816eac83b682d..35dc1b81f906a8 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1116,11 +1116,8 @@ def test_read1_bound_content_length(self): def test_response_fileno(self): # Make sure fd returned by fileno is valid. - serv = socket.socket( - socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP) + serv = socket.bind_socket((HOST, 0)) self.addCleanup(serv.close) - serv.bind((HOST, 0)) - serv.listen() result = None def run_server(): diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 9e571cc78e4b07..bf0b6d38a706c2 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -752,9 +752,7 @@ def test_server_side(self): def test_unknown_channel_binding(self): # should raise ValueError for unknown type - s = socket.socket(socket.AF_INET) - s.bind(('127.0.0.1', 0)) - s.listen() + s = socket.bind_socket(('127.0.0.1', 0)) c = socket.socket(socket.AF_INET) c.connect(s.getsockname()) with test_wrap_socket(c, do_handshake_on_connect=False) as ss: diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 4a8f3c58187213..345622d186d4cc 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -91,14 +91,12 @@ def test_forget(self): support.rmtree('__pycache__') def test_HOST(self): - s = socket.socket() - s.bind((support.HOST, 0)) + s = socket.bind_socket((support.HOST, 0)) s.close() def test_find_unused_port(self): port = support.find_unused_port() - s = socket.socket() - s.bind((support.HOST, port)) + s = socket.bind_socket((support.HOST, port)) s.close() def test_bind_port(self): From cbaa3c129ceb1a021251aca280fcc517007c4802 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 18:59:43 +0100 Subject: [PATCH 04/43] set default backlog to 128; provide a more informative message if SO_REUSEPORT is not supported --- Doc/library/socket.rst | 2 +- Lib/socket.py | 5 ++++- Lib/test/test_socket.py | 7 +++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 07b8ccc0f308ee..590b06469f922c 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -596,7 +596,7 @@ The following functions all create :ref:`socket objects `. *source_address* was added. .. function:: bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, - backlog=100, reuse_port=False): + backlog=128, reuse_port=False): Convenience function which creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call diff --git a/Lib/socket.py b/Lib/socket.py index 9649253d61762e..360756d438562c 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -728,7 +728,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, else: raise error("getaddrinfo returns an empty list") -def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=100, +def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, reuse_port=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -755,11 +755,14 @@ def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=100, family = AF_UNSPEC reuse_addr = hasattr(_socket, 'SO_REUSEADDR') and \ os.name == 'posix' and sys.platform != 'cygwin' + if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): + raise ValueError("SO_REUSEPORT not supported on this platform") info = getaddrinfo(host, port, family, type, 0, AI_PASSIVE) # prefer AF_INET over AF_INET6 if family == AF_UNSPEC: info.sort(key=lambda x: x[0] == AF_INET, reverse=True) err = None + for res in info: af, socktype, proto, canonname, sa = res try: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index f74d64d0cfa70b..7252247fe86b7d 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6084,9 +6084,12 @@ def test_type(self): with socket.bind_socket((None, 0), type=socket.SOCK_DGRAM) as sock: self.assertEqual(sock.type, socket.SOCK_DGRAM) - @unittest.skipIf(not hasattr(socket, "SO_REUSEPORT"), - "SO_REUSEPORT not supported") def test_reuse_port(self): + if not hasattr(socket, "SO_REUSEPORT"): + with self.assertRaises(ValueError, socket): + socket.bind_socket(("127.0.0.1", 0), reuse_port=True) + return + with socket.bind_socket(("127.0.0.1", 0)) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) self.assertEqual(opt, 0) From 98324352d5e8c4c1c98f8a31881384dfd63f6680 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 19:16:11 +0100 Subject: [PATCH 05/43] add 'flags' parameter --- Doc/library/socket.rst | 3 ++- Lib/socket.py | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 590b06469f922c..bd8fccad2ca67b 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -596,7 +596,7 @@ The following functions all create :ref:`socket objects `. *source_address* was added. .. function:: bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, - backlog=128, reuse_port=False): + backlog=128, reuse_port=False, flags=AI_PASSIVE): Convenience function which creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call @@ -607,6 +607,7 @@ The following functions all create :ref:`socket objects `. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if :data:`SOCK_STREAM` *type* is used. + *flags* is a bitmask for :meth:`getaddrinfo()`. .. versionadded:: 3.8 diff --git a/Lib/socket.py b/Lib/socket.py index 360756d438562c..a1f3c7fec23e37 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -729,7 +729,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, raise error("getaddrinfo returns an empty list") def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, - reuse_port=False): + reuse_port=False, flags=getattr(_socket, "AI_PASSIVE", 0)): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -743,6 +743,8 @@ def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, *backlog* is the queue size passed to socket.listen() and is ignored for SOCK_DGRAM socket types. + *flags* is a bitmask for getaddrinfo(). + >>> with bind_socket((None, 8000)) as server: ... while True: ... conn, addr = server.accept() @@ -757,12 +759,12 @@ def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, os.name == 'posix' and sys.platform != 'cygwin' if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") - info = getaddrinfo(host, port, family, type, 0, AI_PASSIVE) - # prefer AF_INET over AF_INET6 + info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: + # prefer AF_INET over AF_INET6 info.sort(key=lambda x: x[0] == AF_INET, reverse=True) - err = None + err = None for res in info: af, socktype, proto, canonname, sa = res try: From 17a6b7b836770196129d6ae3475f9680c564541e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 19:44:58 +0100 Subject: [PATCH 06/43] use bind_socket() in more unit-tests --- Lib/test/eintrdata/eintr_tester.py | 5 +---- Lib/test/test_epoll.py | 4 +--- Lib/test/test_ftplib.py | 9 +++------ Lib/test/test_kqueue.py | 4 +--- Lib/test/test_ssl.py | 7 ++----- 5 files changed, 8 insertions(+), 21 deletions(-) diff --git a/Lib/test/eintrdata/eintr_tester.py b/Lib/test/eintrdata/eintr_tester.py index 25c169bde5005f..da44dc00d4c345 100644 --- a/Lib/test/eintrdata/eintr_tester.py +++ b/Lib/test/eintrdata/eintr_tester.py @@ -284,12 +284,9 @@ def test_sendmsg(self): self._test_send(lambda sock, data: sock.sendmsg([data])) def test_accept(self): - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock = socket.bind_socket((support.HOST, 0)) self.addCleanup(sock.close) - - sock.bind((support.HOST, 0)) port = sock.getsockname()[1] - sock.listen() code = '\n'.join(( 'import socket, time', diff --git a/Lib/test/test_epoll.py b/Lib/test/test_epoll.py index efb54f42deb39e..4f45cdd7a55eb5 100644 --- a/Lib/test/test_epoll.py +++ b/Lib/test/test_epoll.py @@ -41,9 +41,7 @@ class TestEPoll(unittest.TestCase): def setUp(self): - self.serverSocket = socket.socket() - self.serverSocket.bind(('127.0.0.1', 0)) - self.serverSocket.listen() + self.serverSocket = socket.bind_socket(('127.0.0.1', 0)) self.connections = [self.serverSocket] def tearDown(self): diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index da8ba32917be74..5cb253a98a10e4 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -132,9 +132,7 @@ def cmd_port(self, arg): self.push('200 active data connection established') def cmd_pasv(self, arg): - with socket.socket() as sock: - sock.bind((self.socket.getsockname()[0], 0)) - sock.listen() + with socket.bind_socket((self.socket.getsockname()[0], 0)) as sock: sock.settimeout(TIMEOUT) ip, port = sock.getsockname()[:2] ip = ip.replace('.', ','); p1 = port / 256; p2 = port % 256 @@ -150,9 +148,8 @@ def cmd_eprt(self, arg): self.push('200 active data connection established') def cmd_epsv(self, arg): - with socket.socket(socket.AF_INET6) as sock: - sock.bind((self.socket.getsockname()[0], 0)) - sock.listen() + with socket.bind_socket((self.socket.getsockname()[0], 0), + family=socket.AF_INET6) as sock: sock.settimeout(TIMEOUT) port = sock.getsockname()[1] self.push('229 entering extended passive mode (|||%d|)' %port) diff --git a/Lib/test/test_kqueue.py b/Lib/test/test_kqueue.py index 1099c759a7910e..4f41687bd1770b 100644 --- a/Lib/test/test_kqueue.py +++ b/Lib/test/test_kqueue.py @@ -110,9 +110,7 @@ def test_create_event(self): def test_queue_event(self): - serverSocket = socket.socket() - serverSocket.bind(('127.0.0.1', 0)) - serverSocket.listen() + serverSocket = socket.bind_socket(('127.0.0.1', 0)) client = socket.socket() client.setblocking(False) try: diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index bf0b6d38a706c2..52ccef6d1d5261 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -1644,11 +1644,8 @@ def test_subclass(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE - with socket.socket() as s: - s.bind(("127.0.0.1", 0)) - s.listen() - c = socket.socket() - c.connect(s.getsockname()) + with socket.bind_socket(("127.0.0.1", 0)) as s: + c = socket.create_connection(s.getsockname()) c.setblocking(False) with ctx.wrap_socket(c, False, do_handshake_on_connect=False) as c: with self.assertRaises(ssl.SSLWantReadError) as cm: From 29b1d696553969bc1f583c8ce5e3847a67e1327c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 20:11:26 +0100 Subject: [PATCH 07/43] add comment --- Doc/library/socket.rst | 2 +- Lib/socket.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index bd8fccad2ca67b..fc4f15544682f3 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -575,7 +575,7 @@ The following functions all create :ref:`socket objects `. .. function:: create_connection(address[, timeout[, source_address]]) - Connect to a TCP service listening on the internet *address* (a 2-tuple + Connect to a TCP service listening on the Internet *address* (a 2-tuple ``(host, port)``), and return the socket object. This is a higher-level function than :meth:`socket.connect`: if *host* is a non-numeric hostname, it will try to resolve it for both :data:`AF_INET` and :data:`AF_INET6`, diff --git a/Lib/socket.py b/Lib/socket.py index a1f3c7fec23e37..87b61a56c8c4b2 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -752,6 +752,7 @@ def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, """ host, port = address if host == "": + # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html host = None # all interfaces if not family: family = AF_UNSPEC From e4063f2b12e014f1a1629a95ddfb588cf569d269 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 7 Feb 2019 20:25:48 +0100 Subject: [PATCH 08/43] add NEWS entry --- .../NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst diff --git a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst new file mode 100644 index 00000000000000..6abc7373a83a2b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst @@ -0,0 +1 @@ +Add socket.bind_socket() convenience function. (patch by Giampaolo Rodola) From 057d831a072c8d4ea17ae64853a05fb50a3d6955 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Feb 2019 10:21:26 +0100 Subject: [PATCH 09/43] add 'reuse_addr' arg --- Doc/library/socket.rst | 9 ++++++--- Doc/whatsnew/3.8.rst | 2 +- Lib/socket.py | 21 ++++++++++++++------- Lib/test/test_asyncio/functional.py | 2 +- Lib/test/test_socket.py | 15 +++++++++++++++ 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index fc4f15544682f3..3caceef08c6671 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -596,17 +596,20 @@ The following functions all create :ref:`socket objects `. *source_address* was added. .. function:: bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, - backlog=128, reuse_port=False, flags=AI_PASSIVE): + backlog=128, reuse_addr=None, reuse_port=False, + flags=AI_PASSIVE): Convenience function which creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call :meth:`socket.accept()` in order to accept new connections. If *host* is an empty string or ``None`` all network interfaces are assumed. - If *family* is :data:`AF_UNSPEC` or ``None`` the address family will be - determined from the *host* specified in *address*. + If *family* is :data:`AF_UNSPEC` address family will be determined from + the *host* specified in *address*. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if :data:`SOCK_STREAM` *type* is used. + *reuse_addr* and *reuse_port* dictates whether to use :data:`SO_REUSEADDR` + and :data:`SO_REUSEPORT` socket options. *flags* is a bitmask for :meth:`getaddrinfo()`. .. versionadded:: 3.8 diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 660b0548213373..de60a0f6ad8554 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -216,7 +216,7 @@ contain characters unrepresentable at the OS level. socket ------ -Added :meth:`~socket.bind_socket()` a convenience function. +Added :meth:`~socket.bind_socket()` convenience function. (Contributed by Giampaolo Rodola in :issue:`17561`.) diff --git a/Lib/socket.py b/Lib/socket.py index 87b61a56c8c4b2..cf075c112d66ed 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -729,20 +729,24 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, raise error("getaddrinfo returns an empty list") def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, - reuse_port=False, flags=getattr(_socket, "AI_PASSIVE", 0)): + reuse_addr=None, reuse_port=False, + flags=getattr(_socket, "AI_PASSIVE", 0)): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. If *host* is an empty string or None all network interfaces are assumed. - If *family* is AF_UNSPEC or None the address family will be - determined from the *host* specified in *address*. + If *family* is AF_UNSPEC the address family will be determined from the + *host* specified in *address*. *type* should be either SOCK_STREAM or SOCK_DGRAM. *backlog* is the queue size passed to socket.listen() and is ignored for SOCK_DGRAM socket types. + *reuse_addr* and *reuse_port* dictate whether to use SO_REUSEADDR + and SO_REUSEPORT socket options. + *flags* is a bitmask for getaddrinfo(). >>> with bind_socket((None, 8000)) as server: @@ -750,14 +754,16 @@ def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, ... conn, addr = server.accept() ... # handle new connection """ + # --- setup host, port = address if host == "": # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html host = None # all interfaces - if not family: - family = AF_UNSPEC - reuse_addr = hasattr(_socket, 'SO_REUSEADDR') and \ - os.name == 'posix' and sys.platform != 'cygwin' + if reuse_addr is None: + reuse_addr = os.name == 'posix' and sys.platform != 'cygwin' and \ + hasattr(_socket, 'SO_REUSEADDR') + elif reuse_addr and not hasattr(_socket, 'SO_REUSEADDR'): + raise ValueError("SO_REUSEADDR not supported on this platform") if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") info = getaddrinfo(host, port, family, type, 0, flags) @@ -765,6 +771,7 @@ def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, # prefer AF_INET over AF_INET6 info.sort(key=lambda x: x[0] == AF_INET, reverse=True) + # --- implementation err = None for res in info: af, socktype, proto, canonname, sa = res diff --git a/Lib/test/test_asyncio/functional.py b/Lib/test/test_asyncio/functional.py index c79187a34b84d9..868965a9b1919a 100644 --- a/Lib/test/test_asyncio/functional.py +++ b/Lib/test/test_asyncio/functional.py @@ -60,7 +60,7 @@ def tcp_server(self, server_prog, *, else: addr = ('127.0.0.1', 0) - sock = socket.bind_socket(family=family, backlog=backlog) + sock = socket.bind_socket(addr, family=family, backlog=backlog) if timeout is None: raise RuntimeError('timeout is required') if timeout <= 0: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 7252247fe86b7d..cb2aa22328e17c 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6084,6 +6084,21 @@ def test_type(self): with socket.bind_socket((None, 0), type=socket.SOCK_DGRAM) as sock: self.assertEqual(sock.type, socket.SOCK_DGRAM) + def test_reuse_addr(self): + if not hasattr(socket, "SO_REUSEADDR"): + with self.assertRaises(ValueError, socket): + socket.bind_socket(("127.0.0.1", 0), reuse_addr=True) + return + # check False + with socket.bind_socket(("127.0.0.1", 0), reuse_addr=False) as sock: + opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) + self.assertEqual(opt, 0) + port = sock.getsockname()[1] + # Make sure the same port can be reused once socket is closed, + # meaning SO_REUSEADDR is implicitly set by default. + with socket.bind_socket(("127.0.0.1", port)) as sock: + pass + def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): with self.assertRaises(ValueError, socket): From b4883cbc96c58c33ad58de778503486e12b62659 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Feb 2019 17:01:40 +0100 Subject: [PATCH 10/43] rename method --- Lib/test/test_socket.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index cb2aa22328e17c..df1cd4fc4eb141 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6144,7 +6144,7 @@ def run(): self.thread.start() event.set() - def echo_test(self, sock): + def echo_client(self, sock): self.echo_server(sock) server_addr = sock.getsockname()[:2] if sock.type == socket.SOCK_STREAM: @@ -6160,24 +6160,24 @@ def echo_test(self, sock): def test_tcp4(self): with socket.bind_socket(("localhost", 0), socket.AF_INET, socket.SOCK_STREAM) as sock: - self.echo_test(sock) + self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): with socket.bind_socket(("localhost", 0), socket.AF_INET6, socket.SOCK_STREAM) as sock: - self.echo_test(sock) + self.echo_client(sock) def test_udp4(self): with socket.bind_socket(("localhost", 0), socket.AF_INET, socket.SOCK_DGRAM) as sock: - self.echo_test(sock) + self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_udp6(self): with socket.bind_socket(("localhost", 0), socket.AF_INET6, socket.SOCK_DGRAM) as sock: - self.echo_test(sock) + self.echo_client(sock) def test_main(): From fb0e442f28f3e8735b1a1e4d621f4478c2cba3c3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Feb 2019 18:22:35 +0100 Subject: [PATCH 11/43] make family and type kw-only args --- Doc/library/socket.rst | 2 +- Lib/socket.py | 2 +- Lib/test/test_socket.py | 16 ++++++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 3caceef08c6671..2505c070d50f6d 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,7 +595,7 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, +.. function:: bind_socket(address, *. family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=AI_PASSIVE): diff --git a/Lib/socket.py b/Lib/socket.py index cf075c112d66ed..b628817c30c404 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -728,7 +728,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, else: raise error("getaddrinfo returns an empty list") -def bind_socket(address, family=AF_UNSPEC, type=SOCK_STREAM, *, backlog=128, +def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=getattr(_socket, "AI_PASSIVE", 0)): """Convenience function which creates a socket bound to *address* diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index df1cd4fc4eb141..dd35c821e5c61d 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6158,25 +6158,25 @@ def echo_client(self, sock): self.assertEqual(msg, b'foo') def test_tcp4(self): - with socket.bind_socket(("localhost", 0), - socket.AF_INET, socket.SOCK_STREAM) as sock: + with socket.bind_socket(("localhost", 0), family=socket.AF_INET, + type=socket.SOCK_STREAM) as sock: self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): - with socket.bind_socket(("localhost", 0), - socket.AF_INET6, socket.SOCK_STREAM) as sock: + with socket.bind_socket(("localhost", 0), family=socket.AF_INET6, + type=socket.SOCK_STREAM) as sock: self.echo_client(sock) def test_udp4(self): - with socket.bind_socket(("localhost", 0), - socket.AF_INET, socket.SOCK_DGRAM) as sock: + with socket.bind_socket(("localhost", 0), family=socket.AF_INET, + type=socket.SOCK_DGRAM) as sock: self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_udp6(self): - with socket.bind_socket(("localhost", 0), - socket.AF_INET6, socket.SOCK_DGRAM) as sock: + with socket.bind_socket(("localhost", 0), family=socket.AF_INET6, + type=socket.SOCK_DGRAM) as sock: self.echo_client(sock) From 3e876c5bc5bb7cb4a640c514cce2ee1e8f8e77b9 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Feb 2019 18:47:22 +0100 Subject: [PATCH 12/43] raise ValueError if type is not SOCK_STREAM/DGRAM --- Lib/socket.py | 8 ++++---- Lib/test/test_socket.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index b628817c30c404..21e6e47ed5287c 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -729,8 +729,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, raise error("getaddrinfo returns an empty list") def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, - reuse_addr=None, reuse_port=False, - flags=getattr(_socket, "AI_PASSIVE", 0)): + reuse_addr=None, reuse_port=False, flags=AI_PASSIVE): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -754,7 +753,6 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, ... conn, addr = server.accept() ... # handle new connection """ - # --- setup host, port = address if host == "": # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html @@ -766,12 +764,14 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, raise ValueError("SO_REUSEADDR not supported on this platform") if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") + if type not in {SOCK_STREAM, SOCK_DGRAM}: + raise ValueError("only %r and %r types are supported (got %r)" % ( + SOCK_STREAM, SOCK_DGRAM, type)) info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: # prefer AF_INET over AF_INET6 info.sort(key=lambda x: x[0] == AF_INET, reverse=True) - # --- implementation err = None for res in info: af, socktype, proto, canonname, sa = res diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index dd35c821e5c61d..79d81b8b3b85b4 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6083,6 +6083,8 @@ def test_type(self): self.assertEqual(sock.type, socket.SOCK_STREAM) with socket.bind_socket((None, 0), type=socket.SOCK_DGRAM) as sock: self.assertEqual(sock.type, socket.SOCK_DGRAM) + with self.assertRaises(ValueError): + socket.bind_socket((None, 0), type=0) def test_reuse_addr(self): if not hasattr(socket, "SO_REUSEADDR"): From e9fb489bcfb40a10f57794cbafb610549072e42d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Feb 2019 19:15:51 +0100 Subject: [PATCH 13/43] set IPV6_V6ONLY by default --- Doc/library/socket.rst | 33 +++++++++++++++++---------------- Lib/socket.py | 11 +++++++++++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 2505c070d50f6d..6464db62311cf3 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,25 +595,26 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: bind_socket(address, *. family=AF_UNSPEC, type=SOCK_STREAM, - backlog=128, reuse_addr=None, reuse_port=False, - flags=AI_PASSIVE): - - Convenience function which creates a socket bound to *address* (a 2-tuple - ``(host, port)``) and return the socket object upon which you can call - :meth:`socket.accept()` in order to accept new connections. - If *host* is an empty string or ``None`` all network interfaces are assumed. - If *family* is :data:`AF_UNSPEC` address family will be determined from - the *host* specified in *address*. - *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. - *backlog* is the queue size passed to :meth:`socket.listen` if - :data:`SOCK_STREAM` *type* is used. - *reuse_addr* and *reuse_port* dictates whether to use :data:`SO_REUSEADDR` - and :data:`SO_REUSEPORT` socket options. - *flags* is a bitmask for :meth:`getaddrinfo()`. +.. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=AI_PASSIVE) + + Convenience function which creates a socket bound to *address* (a 2-tuple + ``(host, port)``) and return the socket object upon which you can call + :meth:`socket.accept()` in order to accept new connections. + If *host* is an empty string or ``None`` all network interfaces are assumed. + If *family* is :data:`AF_UNSPEC` address family will be determined from + the *host* specified in *address*. + *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. + *backlog* is the queue size passed to :meth:`socket.listen` if + :data:`SOCK_STREAM` *type* is used. + *reuse_addr* and *reuse_port* dictates whether to use :data:`SO_REUSEADDR` + and :data:`SO_REUSEPORT` socket options. + *flags* is a bitmask for :meth:`getaddrinfo()`. .. versionadded:: 3.8 + .. note:: in case of :data:`AF_INET6` family/address :data:`IPV6_V6ONLY` + socket option is set + .. function:: fromfd(fd, family, type, proto=0) Duplicate the file descriptor *fd* (an integer as returned by a file object's diff --git a/Lib/socket.py b/Lib/socket.py index 21e6e47ed5287c..8e125d1c74028a 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -791,6 +791,17 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, pass if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) + # Disable IPv4/IPv6 dual stack support (enabled by default + # on Linux) which makes a single socket listen on both + # address families. Reasons: + # * consistency across different platforms + # * the address returned by getpeername() is an exotic IPv6 + # address that has the IPv4 address encoded inside it + if has_ipv6 and af == AF_INET6: + try: + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) + except NameError: + pass # not supported sock.bind(sa) if socktype == SOCK_STREAM: sock.listen(backlog) From 231455f150e7c034e18f925df502b99734087f9c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 8 Feb 2019 19:18:55 +0100 Subject: [PATCH 14/43] unittest: check IPV6_V6ONLY is set by default --- Lib/socket.py | 2 +- Lib/test/test_socket.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/socket.py b/Lib/socket.py index 8e125d1c74028a..cf6779b3675ba5 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -801,7 +801,7 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, try: sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) except NameError: - pass # not supported + pass sock.bind(sa) if socktype == SOCK_STREAM: sock.listen(backlog) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 79d81b8b3b85b4..dbe45cf601c4d6 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6114,6 +6114,13 @@ def test_reuse_port(self): opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) self.assertEqual(opt, 1) + @unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or + not hasattr(_socket, 'IPV6_V6ONLY'), + "IPV6_V6ONLY option not supported") + def test_ipv6only_default(self): + with socket.bind_socket(("::1", 0)) as sock: + assert sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) + class BindSocketFunctionalTest(unittest.TestCase): timeout = 3 From fbdce4ea035bc2ea4127f0c019a8fdc495b62257 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 11 Feb 2019 19:23:58 +0100 Subject: [PATCH 15/43] fix test failures --- Doc/library/socket.rst | 2 +- Lib/test/test_socket.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 6464db62311cf3..4e5371c8b94f49 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -601,7 +601,7 @@ The following functions all create :ref:`socket objects `. ``(host, port)``) and return the socket object upon which you can call :meth:`socket.accept()` in order to accept new connections. If *host* is an empty string or ``None`` all network interfaces are assumed. - If *family* is :data:`AF_UNSPEC` address family will be determined from + If *family* is :data:`AF_UNSPEC` the address family will be determined from the *host* specified in *address*. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index dbe45cf601c4d6..51672f558e26f1 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6103,7 +6103,7 @@ def test_reuse_addr(self): def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): - with self.assertRaises(ValueError, socket): + with self.assertRaises(ValueError, socket.error): socket.bind_socket(("127.0.0.1", 0), reuse_port=True) return @@ -6112,7 +6112,7 @@ def test_reuse_port(self): self.assertEqual(opt, 0) with socket.bind_socket(("127.0.0.1", 0), reuse_port=True) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) - self.assertEqual(opt, 1) + self.assertNotEqual(opt, 0) @unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or not hasattr(_socket, 'IPV6_V6ONLY'), From 2e9e48c8a3c14370e38f47d60b1080785b71b09a Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 01:34:08 +0100 Subject: [PATCH 16/43] adjust doc wording --- Doc/library/socket.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 4e5371c8b94f49..f0bd931af6d21f 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -612,8 +612,8 @@ The following functions all create :ref:`socket objects `. .. versionadded:: 3.8 - .. note:: in case of :data:`AF_INET6` family/address :data:`IPV6_V6ONLY` - socket option is set + .. note:: in case of :data:`AF_INET6` family or address :data:`IPV6_V6ONLY` + socket option is set. .. function:: fromfd(fd, family, type, proto=0) From 4f28c4702eb4b6a626e2d42f45ff9b33fcf2e593 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 01:35:48 +0100 Subject: [PATCH 17/43] change var name --- Lib/socket.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index cf6779b3675ba5..621b92d1e2de9c 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -777,8 +777,8 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, af, socktype, proto, canonname, sa = res try: sock = socket(af, socktype, proto) - except error as _: - err = _ + except error as _err: + err = _err if err.errno == errno.EAFNOSUPPORT: continue else: From 281b914c56c4a017ad05a7ac35402034eb765e4d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 02:02:46 +0100 Subject: [PATCH 18/43] set flags arg to None by default --- Doc/library/socket.rst | 3 ++- Lib/socket.py | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index f0bd931af6d21f..58292995b34411 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -608,7 +608,8 @@ The following functions all create :ref:`socket objects `. :data:`SOCK_STREAM` *type* is used. *reuse_addr* and *reuse_port* dictates whether to use :data:`SO_REUSEADDR` and :data:`SO_REUSEPORT` socket options. - *flags* is a bitmask for :meth:`getaddrinfo()`. + *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` + :data:`AI_PASSIVE` is used. .. versionadded:: 3.8 diff --git a/Lib/socket.py b/Lib/socket.py index 621b92d1e2de9c..51add15eefe68e 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -729,7 +729,7 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, raise error("getaddrinfo returns an empty list") def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, - reuse_addr=None, reuse_port=False, flags=AI_PASSIVE): + reuse_addr=None, reuse_port=False, flags=None): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -746,7 +746,7 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, *reuse_addr* and *reuse_port* dictate whether to use SO_REUSEADDR and SO_REUSEPORT socket options. - *flags* is a bitmask for getaddrinfo(). + *flags* is a bitmask for getaddrinfo(). If None AI_PASSIVE is used. >>> with bind_socket((None, 8000)) as server: ... while True: @@ -767,6 +767,8 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, if type not in {SOCK_STREAM, SOCK_DGRAM}: raise ValueError("only %r and %r types are supported (got %r)" % ( SOCK_STREAM, SOCK_DGRAM, type)) + if flags is None: + flags = AI_PASSIVE info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: # prefer AF_INET over AF_INET6 From e003dfe82b21a00ac79dc43f8c5778b1eb40f76d Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 02:44:16 +0100 Subject: [PATCH 19/43] document that AF_INET is preferred if host's family is unclear --- Doc/library/socket.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 58292995b34411..c3670b395ace7d 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,19 +595,20 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=AI_PASSIVE) +.. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None) Convenience function which creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call :meth:`socket.accept()` in order to accept new connections. If *host* is an empty string or ``None`` all network interfaces are assumed. If *family* is :data:`AF_UNSPEC` the address family will be determined from - the *host* specified in *address*. + the *host* specified in *address*, and if family can't clearly be determined + from *host* then :data:`AF_INET` will be preferred over :data:`AF_INET6`. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if :data:`SOCK_STREAM` *type* is used. *reuse_addr* and *reuse_port* dictates whether to use :data:`SO_REUSEADDR` - and :data:`SO_REUSEPORT` socket options. + and :data:`SO_REUSEPORT` socket options respectively. *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. From 0a893ca35f4a27645eebef670d8935e4d2612a51 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 03:09:30 +0100 Subject: [PATCH 20/43] introduce supports_hybrid_ipv46 and relative bind_socket arg --- Doc/library/socket.rst | 23 +++++++- Doc/whatsnew/3.8.rst | 4 +- Lib/socket.py | 55 +++++++++++++++---- Lib/test/test_socket.py | 33 ++++++++++- .../2019-02-07-20-25-39.bpo-35934.QmfNmY.rst | 3 +- 5 files changed, 99 insertions(+), 19 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index c3670b395ace7d..80f011f883924c 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,7 +595,7 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None) +.. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None, hybrid_ipv46=False) Convenience function which creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call @@ -612,10 +612,27 @@ The following functions all create :ref:`socket objects `. *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. + When *hybrid_ipv46* is ``True`` and family or address is of :data:`AF_INET6` + kind it will create a socket able to accept both IPv4 and IPv6 connections. + In this case the address returned by :meth:`socket.getpeername` when a new + IPv4 connection occurs will be an IPv6 address represented as an IPv4-mapped + IPv6 address (e.g. ``"::ffff:127.0.0.1"``). + When *hybrid_ipv46* is ``False`` it will explicitly disable this option on + platforms that support it or enable it by default (e.g. Linux). + For platforms not supporting this functionality natively you could use this + `MultipleSocketsListener recipe `__. + This parameter can be used in conjunction with :func:`supports_hybrid_ipv46` + and it only affects :data:`SOCK_STREAM` type sockets, else it's ignored. + .. versionadded:: 3.8 - .. note:: in case of :data:`AF_INET6` family or address :data:`IPV6_V6ONLY` - socket option is set. +.. function:: supports_hybrid_ipv46() + + Return ``True`` if the platform supports creating a single + :data:`SOCK_STREAM` socket which can accept both :data:`AF_INET` and + :data:`AF_INET6` (IPv4 / IPv6) connections. + + .. versionadded:: 3.8 .. function:: fromfd(fd, family, type, proto=0) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index de60a0f6ad8554..42f2828ff6a9d2 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -216,8 +216,8 @@ contain characters unrepresentable at the OS level. socket ------ -Added :meth:`~socket.bind_socket()` convenience function. -(Contributed by Giampaolo Rodola in :issue:`17561`.) +Added :meth:`~socket.bind_socket()` and :meth:`~socket.supports_hybrid_ipv46()` +convenience functions. (Contributed by Giampaolo Rodola in :issue:`17561`.) shutil diff --git a/Lib/socket.py b/Lib/socket.py index 51add15eefe68e..1d898946f5a3fe 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -728,8 +728,24 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, else: raise error("getaddrinfo returns an empty list") +def supports_hybrid_ipv46(): + """Return True if the platform supports creating a SOCK_STREAM socket + which can handle both AF_INET and AF_INET6 (IPv4 / IPv6) connections. + """ + if not has_ipv6 \ + or not hasattr(_socket, 'IPPROTO_IPV6') \ + or not hasattr(_socket, 'IPV6_V6ONLY'): + return False + try: + with socket(AF_INET6, SOCK_STREAM) as sock: + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) + return True + except error: + return False + def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, - reuse_addr=None, reuse_port=False, flags=None): + reuse_addr=None, reuse_port=False, flags=None, + hybrid_ipv46=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -748,6 +764,11 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, *flags* is a bitmask for getaddrinfo(). If None AI_PASSIVE is used. + *hybrid_ipv46*: if True and family or address is of AF_INET6 kind + it will create a socket able to accept both IPv4 and IPv6 connections. + When False it will explicitly disable this option on platforms that + support it or enable it by default (e.g. Linux). + >>> with bind_socket((None, 8000)) as server: ... while True: ... conn, addr = server.accept() @@ -769,6 +790,12 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, SOCK_STREAM, SOCK_DGRAM, type)) if flags is None: flags = AI_PASSIVE + if hybrid_ipv46: + if type != SOCK_STREAM: + raise ValueError("hybrid_ipv46 is allowed with %r type only " + "(got %r)" % (SOCK_STREAM, type)) + if not supports_hybrid_ipv46(): + raise ValueError("hybrid_ipv46 not supported on this platform") info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: # prefer AF_INET over AF_INET6 @@ -793,17 +820,21 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, pass if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) - # Disable IPv4/IPv6 dual stack support (enabled by default - # on Linux) which makes a single socket listen on both - # address families. Reasons: - # * consistency across different platforms - # * the address returned by getpeername() is an exotic IPv6 - # address that has the IPv4 address encoded inside it - if has_ipv6 and af == AF_INET6: - try: - sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) - except NameError: - pass + if has_ipv6 and af == AF_INET6 and type == SOCK_STREAM: + if not hybrid_ipv46: + # Disable IPv4/IPv6 dual stack support (enabled by + # default on Linux) which makes a single socket + # listen on both address families. Reasons: + # * consistency across different platforms + # * the address returned by getpeername() is an + # exotic IPv6 address that has the IPv4 address + # encoded inside it + try: + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) + except NameError: + pass + else: + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) sock.bind(sa) if socktype == SOCK_STREAM: sock.listen(backlog) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 51672f558e26f1..c28b8deef3dc3e 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6153,9 +6153,13 @@ def run(): self.thread.start() event.set() - def echo_client(self, sock): + def echo_client(self, sock, connect_host=None): self.echo_server(sock) server_addr = sock.getsockname()[:2] + if connect_host is None: + connect_host = sock.getsockname()[0] + port = sock.getsockname()[1] + server_addr = (connect_host, port) if sock.type == socket.SOCK_STREAM: with socket.create_connection(server_addr) as client: client.sendall(b'foo') @@ -6188,6 +6192,33 @@ def test_udp6(self): type=socket.SOCK_DGRAM) as sock: self.echo_client(sock) + # --- + # Dual-stack IPv4/6 tests: create a hybrid IPv4/6 server and + # connect with a client using IPv4 and IPv6 addresses. + # --- + + @unittest.skipIf(not socket.supports_hybrid_ipv46(), + "hybrid_ipv46 not supported") + def test_dual_stack_tcp4(self): + with socket.bind_socket((None, 0), family=socket.AF_INET6, + type=socket.SOCK_STREAM, + hybrid_ipv46=True) as sock: + self.echo_client(sock, connect_host="127.0.0.1") + + @unittest.skipIf(not socket.supports_hybrid_ipv46(), + "hybrid_ipv46 not supported") + def test_dual_stack_tcp6(self): + with socket.bind_socket((None, 0), family=socket.AF_INET6, + type=socket.SOCK_STREAM, + hybrid_ipv46=True) as sock: + self.echo_client(sock, connect_host="::1") + + def test_dual_stack_udp4(self): + # Hybrid IPv4&6 UDP servers won't allow IPv4 connections. + with self.assertRaises(ValueError): + socket.bind_socket((None, 0), family=socket.AF_INET6, + type=socket.SOCK_DGRAM, hybrid_ipv46=True) + def test_main(): tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest, diff --git a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst index 6abc7373a83a2b..ecb7ef8f6773e0 100644 --- a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst +++ b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst @@ -1 +1,2 @@ -Add socket.bind_socket() convenience function. (patch by Giampaolo Rodola) +Add socket.bind_socket() and socket.supports_hybrid_ipv46() convenience +functions. (patch by Giampaolo Rodola) From 2d247a2d8b6e0859840004f38bcb26df07d5f8af Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 04:18:20 +0100 Subject: [PATCH 21/43] various improvements: - update doc examples - force AF_INET6 if hybrid_ipv46=True - add new functions to __all__ - add more tests --- Doc/library/socket.rst | 41 ++++++++++++++++++- Doc/whatsnew/3.8.rst | 5 ++- Lib/socket.py | 15 ++++--- Lib/test/test_socket.py | 22 ++++++++++ .../2019-02-07-20-25-39.bpo-35934.QmfNmY.rst | 6 ++- 5 files changed, 78 insertions(+), 11 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 80f011f883924c..99df6d6209820f 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -600,10 +600,13 @@ The following functions all create :ref:`socket objects `. Convenience function which creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call :meth:`socket.accept()` in order to accept new connections. + Internally it relies on :meth:`getaddrinfo()` and returns the first socket + which can be bound to *address*. If *host* is an empty string or ``None`` all network interfaces are assumed. If *family* is :data:`AF_UNSPEC` the address family will be determined from - the *host* specified in *address*, and if family can't clearly be determined - from *host* then :data:`AF_INET` will be preferred over :data:`AF_INET6`. + the *host* specified in *address*. If family can't clearly be determined + from *host* and *hybrid_ipv46* is ``False`` then :data:`AF_INET` will be + preferred over :data:`AF_INET6`. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if :data:`SOCK_STREAM` *type* is used. @@ -1817,6 +1820,40 @@ sends traffic to the first one connected successfully. :: print('Received', repr(data)) +The two examples above can be rewritten by using :meth:`socket.bind_socket` +and :meth:`socket.create_connection` convenience functions. +:meth:`socket.bind_socket` has the extra advantage of creating an agnostic +IPv4/IPv6 server on platforms supporting this functionality. + +:: + + # Echo server program + import socket + + HOST = None + PORT = 50007 + s = socket.bind_socket((HOST, PORT), hybrid_ipv46=socket.supports_hybrid_ipv46()) + conn, addr = s.accept() + with conn: + print('Connected by', addr) + while True: + data = conn.recv(1024) + if not data: break + conn.send(data) + +:: + + # Echo client program + import socket + + HOST = 'daring.cwi.nl' + PORT = 50007 + with socket.create_connection((HOST, PORT)) as s: + s.sendall(b'Hello, world') + data = s.recv(1024) + print('Received', repr(data)) + + The next example shows how to write a very simple network sniffer with raw sockets on Windows. The example requires administrator privileges to modify the interface:: diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 42f2828ff6a9d2..413419d3d850fe 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -213,11 +213,14 @@ pathlib contain characters unrepresentable at the OS level. (Contributed by Serhiy Storchaka in :issue:`33721`.) + socket ------ Added :meth:`~socket.bind_socket()` and :meth:`~socket.supports_hybrid_ipv46()` -convenience functions. (Contributed by Giampaolo Rodola in :issue:`17561`.) +convenience functions to automatize the necessary tasks usually involved when +creating a server socket, including accepting both IPv4 and IPv6 connections +on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) shutil diff --git a/Lib/socket.py b/Lib/socket.py index 1d898946f5a3fe..88f662eee14bc8 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -60,8 +60,8 @@ EAGAIN = getattr(errno, 'EAGAIN', 11) EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11) -__all__ = ["fromfd", "getfqdn", "create_connection", - "AddressFamily", "SocketKind"] +__all__ = ["fromfd", "getfqdn", "create_connection", "bind_socket", + "supports_hybrid_ipv46", "AddressFamily", "SocketKind"] __all__.extend(os._get_exports_list(_socket)) # Set up the socket.AF_* socket.SOCK_* constants as members of IntEnums for @@ -786,16 +786,17 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") if type not in {SOCK_STREAM, SOCK_DGRAM}: - raise ValueError("only %r and %r types are supported (got %r)" % ( - SOCK_STREAM, SOCK_DGRAM, type)) + raise ValueError("only SOCK_STREAM and SOCK_DGRAM types are supported") if flags is None: flags = AI_PASSIVE if hybrid_ipv46: if type != SOCK_STREAM: - raise ValueError("hybrid_ipv46 is allowed with %r type only " - "(got %r)" % (SOCK_STREAM, type)) + raise ValueError("hybrid_ipv46 requires SOCK_STREAM type") + if family not in {AF_INET6, AF_UNSPEC}: + raise ValueError("hybrid_ipv46 requires AF_INET family") if not supports_hybrid_ipv46(): raise ValueError("hybrid_ipv46 not supported on this platform") + family = AF_INET6 info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: # prefer AF_INET over AF_INET6 @@ -840,6 +841,8 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, sock.listen(backlog) # Break explicitly a reference cycle. err = None + if hybrid_ipv46 and sock.family != AF_INET6: + raise error("was not able to create an AF_INET6 socket by using address") return sock except error: sock.close() diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index c28b8deef3dc3e..ec979508037878 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6062,6 +6062,18 @@ def test_address(self): self.assertEqual(sock.getsockname()[0], "127.0.0.1") self.assertEqual(sock.getsockname()[1], port) + def test_address_all_nics(self): + # host in (None, "") == 'all NICs' + with socket.bind_socket(("", 0)) as sock: + self.assertEqual(sock.getsockname()[0], "0.0.0.0") + with socket.bind_socket((None, 0)) as sock: + self.assertEqual(sock.getsockname()[0], "0.0.0.0") + if support.IPV6_ENABLED: + with socket.bind_socket(("", 0), family=socket.AF_INET6) as sock: + self.assertEqual(sock.getsockname()[0], "::") + with socket.bind_socket((None, 0), family=socket.AF_INET6) as sock: + self.assertEqual(sock.getsockname()[0], "::") + def test_family(self): # determined by address with socket.bind_socket(("127.0.0.1", 0)) as sock: @@ -6121,6 +6133,16 @@ def test_ipv6only_default(self): with socket.bind_socket(("::1", 0)) as sock: assert sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) + def test_ipv4_default(self): + with socket.bind_socket(("localhost", 0)) as sock: + self.assertEqual(sock.family, socket.AF_INET) + + @unittest.skipIf(not socket.supports_hybrid_ipv46(), + "hybrid_ipv46 not supported") + def test_hybrid_ipv6_default(self): + with socket.bind_socket(("localhost", 0), hybrid_ipv46=True) as sock: + self.assertEqual(sock.family, socket.AF_INET6) + class BindSocketFunctionalTest(unittest.TestCase): timeout = 3 diff --git a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst index ecb7ef8f6773e0..62c3c25aa1372f 100644 --- a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst +++ b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst @@ -1,2 +1,4 @@ -Add socket.bind_socket() and socket.supports_hybrid_ipv46() convenience -functions. (patch by Giampaolo Rodola) +Added :meth:`~socket.bind_socket()` and :meth:`~socket.supports_hybrid_ipv46()` +convenience functions to automatize the necessary tasks usually involved when +creating a server socket, including accepting both IPv4 and IPv6 connections +on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) From 2c3c85c1c4ea705006c3d64c16078719096d9338 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 04:44:09 +0100 Subject: [PATCH 22/43] update doc --- Doc/library/socket.rst | 22 ++++++++++------------ Lib/socket.py | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 99df6d6209820f..36946e37f5e40e 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -605,8 +605,8 @@ The following functions all create :ref:`socket objects `. If *host* is an empty string or ``None`` all network interfaces are assumed. If *family* is :data:`AF_UNSPEC` the address family will be determined from the *host* specified in *address*. If family can't clearly be determined - from *host* and *hybrid_ipv46* is ``False`` then :data:`AF_INET` will be - preferred over :data:`AF_INET6`. + from *host* and *hybrid_ipv46* is ``False`` then :data:`AF_INET` family will + be preferred over :data:`AF_INET6`. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if :data:`SOCK_STREAM` *type* is used. @@ -615,25 +615,23 @@ The following functions all create :ref:`socket objects `. *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. - When *hybrid_ipv46* is ``True`` and family or address is of :data:`AF_INET6` - kind it will create a socket able to accept both IPv4 and IPv6 connections. + If *hybrid_ipv46* is ``True`` and the platform supports it the socket will + be able to accept both IPv4 and IPv6 connections. In this case the address returned by :meth:`socket.getpeername` when a new - IPv4 connection occurs will be an IPv6 address represented as an IPv4-mapped - IPv6 address (e.g. ``"::ffff:127.0.0.1"``). - When *hybrid_ipv46* is ``False`` it will explicitly disable this option on - platforms that support it or enable it by default (e.g. Linux). + IPv4 connection is accepted will be an IPv6 address represented as an + IPv4-mapped IPv6 address like ``"::ffff:127.0.0.1"``. + If *hybrid_ipv46* is ``False`` it will explicitly disable this option on + platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this `MultipleSocketsListener recipe `__. - This parameter can be used in conjunction with :func:`supports_hybrid_ipv46` - and it only affects :data:`SOCK_STREAM` type sockets, else it's ignored. + This parameter can be used in conjunction with :func:`supports_hybrid_ipv46`. .. versionadded:: 3.8 .. function:: supports_hybrid_ipv46() Return ``True`` if the platform supports creating a single - :data:`SOCK_STREAM` socket which can accept both :data:`AF_INET` and - :data:`AF_INET6` (IPv4 / IPv6) connections. + :data:`SOCK_STREAM` socket which can accept both IPv4 and IPv6 connections. .. versionadded:: 3.8 diff --git a/Lib/socket.py b/Lib/socket.py index 88f662eee14bc8..f0e08922f76199 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -752,7 +752,8 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, If *host* is an empty string or None all network interfaces are assumed. If *family* is AF_UNSPEC the address family will be determined from the - *host* specified in *address*. + *host* specified in *address* and AF_INET will take precedence over + AF_INET6 in case family can't be determined from *host*. *type* should be either SOCK_STREAM or SOCK_DGRAM. @@ -762,12 +763,12 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, *reuse_addr* and *reuse_port* dictate whether to use SO_REUSEADDR and SO_REUSEPORT socket options. - *flags* is a bitmask for getaddrinfo(). If None AI_PASSIVE is used. + *flags* is a bitmask for getaddrinfo(). - *hybrid_ipv46*: if True and family or address is of AF_INET6 kind - it will create a socket able to accept both IPv4 and IPv6 connections. + *hybrid_ipv46*: if True and the platform supports it it will create + a socket able to accept both IPv4 and IPv6 connections. When False it will explicitly disable this option on platforms that - support it or enable it by default (e.g. Linux). + enable it by default (e.g. Linux). >>> with bind_socket((None, 8000)) as server: ... while True: @@ -793,10 +794,11 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, if type != SOCK_STREAM: raise ValueError("hybrid_ipv46 requires SOCK_STREAM type") if family not in {AF_INET6, AF_UNSPEC}: - raise ValueError("hybrid_ipv46 requires AF_INET family") + raise ValueError("hybrid_ipv46 requires AF_INET6 family") if not supports_hybrid_ipv46(): raise ValueError("hybrid_ipv46 not supported on this platform") family = AF_INET6 + info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: # prefer AF_INET over AF_INET6 @@ -818,6 +820,8 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, try: sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) except error: + # Fail later on on bind() for platforms which may not + # support this option. pass if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) @@ -841,8 +845,6 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, sock.listen(backlog) # Break explicitly a reference cycle. err = None - if hybrid_ipv46 and sock.family != AF_INET6: - raise error("was not able to create an AF_INET6 socket by using address") return sock except error: sock.close() From 1931e7c3582f3c4e7fffba96276a5a5fd93dc07c Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Tue, 12 Feb 2019 04:52:22 +0100 Subject: [PATCH 23/43] use 'localhost' in tests (safer) --- Lib/test/test_socket.py | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index ec979508037878..ce883147ff4c43 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6091,12 +6091,13 @@ def test_family(self): self.assertEqual(sock.family, socket.AF_INET6) def test_type(self): - with socket.bind_socket((None, 0)) as sock: + with socket.bind_socket(("localhost", 0)) as sock: self.assertEqual(sock.type, socket.SOCK_STREAM) - with socket.bind_socket((None, 0), type=socket.SOCK_DGRAM) as sock: + with socket.bind_socket(("localhost", 0), + type=socket.SOCK_DGRAM) as sock: self.assertEqual(sock.type, socket.SOCK_DGRAM) with self.assertRaises(ValueError): - socket.bind_socket((None, 0), type=0) + socket.bind_socket(("localhost", 0), type=0) def test_reuse_addr(self): if not hasattr(socket, "SO_REUSEADDR"): @@ -6118,7 +6119,6 @@ def test_reuse_port(self): with self.assertRaises(ValueError, socket.error): socket.bind_socket(("127.0.0.1", 0), reuse_port=True) return - with socket.bind_socket(("127.0.0.1", 0)) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) self.assertEqual(opt, 0) @@ -6143,6 +6143,12 @@ def test_hybrid_ipv6_default(self): with socket.bind_socket(("localhost", 0), hybrid_ipv46=True) as sock: self.assertEqual(sock.family, socket.AF_INET6) + def test_dual_stack_udp(self): + # Hybrid IPv4/6 UDP servers won't allow IPv4 connections. + with self.assertRaises(ValueError): + socket.bind_socket(("localhost", 0), type=socket.SOCK_DGRAM, + hybrid_ipv46=True) + class BindSocketFunctionalTest(unittest.TestCase): timeout = 3 @@ -6222,25 +6228,15 @@ def test_udp6(self): @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_dual_stack_tcp4(self): - with socket.bind_socket((None, 0), family=socket.AF_INET6, - type=socket.SOCK_STREAM, - hybrid_ipv46=True) as sock: + with socket.bind_socket((None, 0), hybrid_ipv46=True) as sock: self.echo_client(sock, connect_host="127.0.0.1") @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_dual_stack_tcp6(self): - with socket.bind_socket((None, 0), family=socket.AF_INET6, - type=socket.SOCK_STREAM, - hybrid_ipv46=True) as sock: + with socket.bind_socket((None, 0), hybrid_ipv46=True) as sock: self.echo_client(sock, connect_host="::1") - def test_dual_stack_udp4(self): - # Hybrid IPv4&6 UDP servers won't allow IPv4 connections. - with self.assertRaises(ValueError): - socket.bind_socket((None, 0), family=socket.AF_INET6, - type=socket.SOCK_DGRAM, hybrid_ipv46=True) - def test_main(): tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest, From 2364a89c917d6f55c6b277392ecc52c9f78a3d54 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Feb 2019 17:39:15 +0100 Subject: [PATCH 24/43] raise error on Windows if reuse_addr=True and type != SOCK_DGRAM --- Lib/socket.py | 30 ++++++++++++++++++++---------- Lib/test/test_socket.py | 8 ++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index f0e08922f76199..25f5245dcfb976 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -779,14 +779,27 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, if host == "": # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html host = None # all interfaces + # Note about Windows: by default SO_REUSEADDR is not set because: + # 1) It's unnecessary: bind() will succeed even in case of a + # previous closed socket on the same address and still in TIME_WAIT + # state. + # 2) If set, another socket will be free to bind() on the same + # address, effectively preventing this one from accepting connections. + # Also, it sets the process in a state where it'll no longer respond + # to any signals or graceful kills. The option can still be set though + # for SOCK_DGRAM multicast sockets. + # See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx + iswin = os.name in ('nt', 'cygwin') if reuse_addr is None: - reuse_addr = os.name == 'posix' and sys.platform != 'cygwin' and \ - hasattr(_socket, 'SO_REUSEADDR') + reuse_addr = not iswin and hasattr(_socket, 'SO_REUSEADDR') elif reuse_addr and not hasattr(_socket, 'SO_REUSEADDR'): raise ValueError("SO_REUSEADDR not supported on this platform") + elif reuse_addr and iswin and type != SOCK_DGRAM: + raise ValueError("SO_REUSEADDR allowed for SOCK_DGRAM type only") + if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") - if type not in {SOCK_STREAM, SOCK_DGRAM}: + if type not in (SOCK_STREAM, SOCK_DGRAM): raise ValueError("only SOCK_STREAM and SOCK_DGRAM types are supported") if flags is None: flags = AI_PASSIVE @@ -820,20 +833,17 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, try: sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) except error: - # Fail later on on bind() for platforms which may not + # Fail later on on bind(), for platforms which may not # support this option. pass if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) - if has_ipv6 and af == AF_INET6 and type == SOCK_STREAM: + if has_ipv6 and af == AF_INET6: if not hybrid_ipv46: # Disable IPv4/IPv6 dual stack support (enabled by # default on Linux) which makes a single socket - # listen on both address families. Reasons: - # * consistency across different platforms - # * the address returned by getpeername() is an - # exotic IPv6 address that has the IPv4 address - # encoded inside it + # listen on both address families in order to be + # consistent across different platforms. try: sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) except NameError: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index ce883147ff4c43..504914e515543c 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6114,6 +6114,14 @@ def test_reuse_addr(self): with socket.bind_socket(("127.0.0.1", port)) as sock: pass + @unittest.skipIf(os.name not in ('nt', 'cygwin'), "Windows only") + def test_reuse_addr_win(self): + with self.assertRaises(ValueError): + socket.bind_socket(("localhost, 0"), reuse_addr=True) + s = socket.bind_socket(("localhost, 0"), reuse_addr=True, + type=socket.DGRAM) + s.close() + def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): with self.assertRaises(ValueError, socket.error): From 1d13a9cb420eb14b3ac8c49b58ff7515721cfde6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 13 Feb 2019 22:07:22 +0100 Subject: [PATCH 25/43] update doc + provide better error message on bind() --- Doc/library/socket.rst | 28 ++++++++++++++++++++++++---- Lib/socket.py | 7 ++++++- Lib/test/test_socket.py | 24 ++++++++++++------------ 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 36946e37f5e40e..dbb78259c0057e 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -597,9 +597,12 @@ The following functions all create :ref:`socket objects `. .. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None, hybrid_ipv46=False) - Convenience function which creates a socket bound to *address* (a 2-tuple - ``(host, port)``) and return the socket object upon which you can call - :meth:`socket.accept()` in order to accept new connections. + Convenience function which aims at automating all the typical steps needed + when creating a server socket. + It creates a socket bound to *address* (a 2-tuple ``(host, port)``) and + return the socket object upon which you can call :meth:`socket.accept()` in + order to accept new connections. + Internally it relies on :meth:`getaddrinfo()` and returns the first socket which can be bound to *address*. If *host* is an empty string or ``None`` all network interfaces are assumed. @@ -619,13 +622,30 @@ The following functions all create :ref:`socket objects `. be able to accept both IPv4 and IPv6 connections. In this case the address returned by :meth:`socket.getpeername` when a new IPv4 connection is accepted will be an IPv6 address represented as an - IPv4-mapped IPv6 address like ``"::ffff:127.0.0.1"``. + IPv4-mapped IPv6 address like ``":ffff:127.0.0.1"``. If *hybrid_ipv46* is ``False`` it will explicitly disable this option on platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this `MultipleSocketsListener recipe `__. This parameter can be used in conjunction with :func:`supports_hybrid_ipv46`. + Here's an echo server example listening on all interfaces, port 8888, + and (possibly) hybrid IPv4/IPv6 support: + + :: + + import socket + + with socket.bind_socket(("", 8888), + hybrid_ipv46=socket.supports_hybrid_ipv46()) as server: + conn, addr = server.accept() + with conn: + while True: + data = conn.recv(1024) + if not data: + break + conn.send(data) + .. versionadded:: 3.8 .. function:: supports_hybrid_ipv46() diff --git a/Lib/socket.py b/Lib/socket.py index 25f5245dcfb976..11394bcbb8e6f9 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -850,7 +850,12 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, pass else: sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) - sock.bind(sa) + try: + sock.bind(sa) + except error as err: + es = '%s (while attempting to bind on address %r)' % \ + (err.strerror, sa) + raise error(err.errno, es) from None if socktype == SOCK_STREAM: sock.listen(backlog) # Break explicitly a reference cycle. diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 504914e515543c..c091bfeb6ac166 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6102,16 +6102,16 @@ def test_type(self): def test_reuse_addr(self): if not hasattr(socket, "SO_REUSEADDR"): with self.assertRaises(ValueError, socket): - socket.bind_socket(("127.0.0.1", 0), reuse_addr=True) + socket.bind_socket(("localhost", 0), reuse_addr=True) return # check False - with socket.bind_socket(("127.0.0.1", 0), reuse_addr=False) as sock: + with socket.bind_socket(("localhost", 0), reuse_addr=False) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) self.assertEqual(opt, 0) port = sock.getsockname()[1] # Make sure the same port can be reused once socket is closed, # meaning SO_REUSEADDR is implicitly set by default. - with socket.bind_socket(("127.0.0.1", port)) as sock: + with socket.bind_socket(("localhost", port)) as sock: pass @unittest.skipIf(os.name not in ('nt', 'cygwin'), "Windows only") @@ -6119,20 +6119,20 @@ def test_reuse_addr_win(self): with self.assertRaises(ValueError): socket.bind_socket(("localhost, 0"), reuse_addr=True) s = socket.bind_socket(("localhost, 0"), reuse_addr=True, - type=socket.DGRAM) + type=socket.SOCK_DGRAM) s.close() def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): with self.assertRaises(ValueError, socket.error): - socket.bind_socket(("127.0.0.1", 0), reuse_port=True) - return - with socket.bind_socket(("127.0.0.1", 0)) as sock: - opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) - self.assertEqual(opt, 0) - with socket.bind_socket(("127.0.0.1", 0), reuse_port=True) as sock: - opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) - self.assertNotEqual(opt, 0) + socket.bind_socket(("localhost", 0), reuse_port=True) + else: + with socket.bind_socket(("localhost", 0)) as sock: + opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) + self.assertEqual(opt, 0) + with socket.bind_socket(("localhost", 0), reuse_port=True) as sock: + opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) + self.assertNotEqual(opt, 0) @unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or not hasattr(_socket, 'IPV6_V6ONLY'), From 46a562efc459592ebd2cb81a3282c1a30fe20d00 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 12:50:34 +0100 Subject: [PATCH 26/43] rename bind_socket() to create_server() --- Doc/library/socket.rst | 10 +-- Doc/whatsnew/3.8.rst | 2 +- Lib/ftplib.py | 2 +- Lib/idlelib/rpc.py | 2 +- Lib/socket.py | 10 +-- Lib/test/_test_multiprocessing.py | 4 +- Lib/test/eintrdata/eintr_tester.py | 2 +- Lib/test/test_asyncio/functional.py | 2 +- Lib/test/test_asyncio/test_base_events.py | 2 +- Lib/test/test_asyncio/test_events.py | 6 +- Lib/test/test_asyncio/test_streams.py | 6 +- Lib/test/test_epoll.py | 2 +- Lib/test/test_ftplib.py | 6 +- Lib/test/test_httplib.py | 2 +- Lib/test/test_kqueue.py | 2 +- Lib/test/test_socket.py | 84 +++++++++---------- Lib/test/test_ssl.py | 4 +- Lib/test/test_support.py | 4 +- .../2019-02-07-20-25-39.bpo-35934.QmfNmY.rst | 2 +- 19 files changed, 77 insertions(+), 77 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index dbb78259c0057e..4c56d79a4aa213 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,7 +595,7 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None, hybrid_ipv46=False) +.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None, hybrid_ipv46=False) Convenience function which aims at automating all the typical steps needed when creating a server socket. @@ -636,7 +636,7 @@ The following functions all create :ref:`socket objects `. import socket - with socket.bind_socket(("", 8888), + with socket.create_server(("", 8888), hybrid_ipv46=socket.supports_hybrid_ipv46()) as server: conn, addr = server.accept() with conn: @@ -1838,9 +1838,9 @@ sends traffic to the first one connected successfully. :: print('Received', repr(data)) -The two examples above can be rewritten by using :meth:`socket.bind_socket` +The two examples above can be rewritten by using :meth:`socket.create_server` and :meth:`socket.create_connection` convenience functions. -:meth:`socket.bind_socket` has the extra advantage of creating an agnostic +:meth:`socket.create_server` has the extra advantage of creating an agnostic IPv4/IPv6 server on platforms supporting this functionality. :: @@ -1850,7 +1850,7 @@ IPv4/IPv6 server on platforms supporting this functionality. HOST = None PORT = 50007 - s = socket.bind_socket((HOST, PORT), hybrid_ipv46=socket.supports_hybrid_ipv46()) + s = socket.create_server((HOST, PORT), hybrid_ipv46=socket.supports_hybrid_ipv46()) conn, addr = s.accept() with conn: print('Connected by', addr) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index 413419d3d850fe..a0452ee7561125 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -217,7 +217,7 @@ contain characters unrepresentable at the OS level. socket ------ -Added :meth:`~socket.bind_socket()` and :meth:`~socket.supports_hybrid_ipv46()` +Added :meth:`~socket.create_server()` and :meth:`~socket.supports_hybrid_ipv46()` convenience functions to automatize the necessary tasks usually involved when creating a server socket, including accepting both IPv4 and IPv6 connections on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) diff --git a/Lib/ftplib.py b/Lib/ftplib.py index 1691c20e3fe75f..5910ca0a17c94c 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -302,7 +302,7 @@ def sendeprt(self, host, port): def makeport(self): '''Create a new socket and send a PORT command for it.''' - sock = socket.bind_socket((None, 0), family=self.af, backlog=1) + sock = socket.create_server((None, 0), family=self.af, backlog=1) port = sock.getsockname()[1] # Get proper port host = self.sock.getsockname()[0] # Get proper host if self.af == socket.AF_INET: diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index b9ed4c315642b7..34a403905017d3 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -530,7 +530,7 @@ class RPCClient(SocketIO): nextseq = 1 # Requests coming from the client are odd numbered def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM): - self.listening_sock = socket.bind_socket( + self.listening_sock = socket.create_server( family=family, type=type, backlog=1) def accept(self): diff --git a/Lib/socket.py b/Lib/socket.py index 11394bcbb8e6f9..8333ebe6b9c332 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -60,7 +60,7 @@ EAGAIN = getattr(errno, 'EAGAIN', 11) EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11) -__all__ = ["fromfd", "getfqdn", "create_connection", "bind_socket", +__all__ = ["fromfd", "getfqdn", "create_connection", "create_server", "supports_hybrid_ipv46", "AddressFamily", "SocketKind"] __all__.extend(os._get_exports_list(_socket)) @@ -743,9 +743,9 @@ def supports_hybrid_ipv46(): except error: return False -def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, - reuse_addr=None, reuse_port=False, flags=None, - hybrid_ipv46=False): +def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, + reuse_addr=None, reuse_port=False, flags=None, + hybrid_ipv46=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -770,7 +770,7 @@ def bind_socket(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, When False it will explicitly disable this option on platforms that enable it by default (e.g. Linux). - >>> with bind_socket((None, 8000)) as server: + >>> with create_server((None, 8000)) as server: ... while True: ... conn, addr = server.accept() ... # handle new connection diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index aebcd81c2391ba..30788738b51f32 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -3323,7 +3323,7 @@ def _listener(cls, conn, families): new_conn.close() l.close() - l = socket.bind_socket((test.support.HOST, 0)) + l = socket.create_server((test.support.HOST, 0)) conn.send(l.getsockname()) new_conn, addr = l.accept() conn.send(new_conn) @@ -4075,7 +4075,7 @@ def _child_test_wait_socket(cls, address, slow): def test_wait_socket(self, slow=False): from multiprocessing.connection import wait - l = socket.bind_socket((test.support.HOST, 0)) + l = socket.create_server((test.support.HOST, 0)) addr = l.getsockname() readers = [] procs = [] diff --git a/Lib/test/eintrdata/eintr_tester.py b/Lib/test/eintrdata/eintr_tester.py index da44dc00d4c345..80df1276b204ef 100644 --- a/Lib/test/eintrdata/eintr_tester.py +++ b/Lib/test/eintrdata/eintr_tester.py @@ -284,7 +284,7 @@ def test_sendmsg(self): self._test_send(lambda sock, data: sock.sendmsg([data])) def test_accept(self): - sock = socket.bind_socket((support.HOST, 0)) + sock = socket.create_server((support.HOST, 0)) self.addCleanup(sock.close) port = sock.getsockname()[1] diff --git a/Lib/test/test_asyncio/functional.py b/Lib/test/test_asyncio/functional.py index 868965a9b1919a..70cd140f479669 100644 --- a/Lib/test/test_asyncio/functional.py +++ b/Lib/test/test_asyncio/functional.py @@ -60,7 +60,7 @@ def tcp_server(self, server_prog, *, else: addr = ('127.0.0.1', 0) - sock = socket.bind_socket(addr, family=family, backlog=backlog) + sock = socket.create_server(addr, family=family, backlog=backlog) if timeout is None: raise RuntimeError('timeout is required') if timeout <= 0: diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index a615c5d6c4cfbf..1250e48e19e2df 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1641,7 +1641,7 @@ class Err(OSError): self.assertTrue(m_sock.close.called) def test_create_datagram_endpoint_sock(self): - sock = socket.bind_socket(('127.0.0.1', 0), type=socket.SOCK_DGRAM) + sock = socket.create_server(('127.0.0.1', 0), type=socket.SOCK_DGRAM) fut = self.loop.create_datagram_endpoint( lambda: MyDatagramProto(create_future=True, loop=self.loop), sock=sock) diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index 207e61cdf743ba..b46b614e556eae 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -667,7 +667,7 @@ def data_received(self, data): super().data_received(data) self.transport.write(expected_response) - lsock = socket.bind_socket(('127.0.0.1', 0), backlog=1) + lsock = socket.create_server(('127.0.0.1', 0), backlog=1) addr = lsock.getsockname() message = b'test data' @@ -1116,7 +1116,7 @@ def connection_made(self, transport): super().connection_made(transport) proto.set_result(self) - sock_ob = socket.bind_socket(('0.0.0.0', 0)) + sock_ob = socket.create_server(('0.0.0.0', 0)) f = self.loop.create_server(TestMyProto, sock=sock_ob) server = self.loop.run_until_complete(f) @@ -1132,7 +1132,7 @@ def connection_made(self, transport): server.close() def test_create_server_addr_in_use(self): - sock_ob = socket.bind_socket(('0.0.0.0', 0)) + sock_ob = socket.create_server(('0.0.0.0', 0)) f = self.loop.create_server(MyProto, sock=sock_ob) server = self.loop.run_until_complete(f) diff --git a/Lib/test/test_asyncio/test_streams.py b/Lib/test/test_asyncio/test_streams.py index f7846e4c843388..630f91dbf4780f 100644 --- a/Lib/test/test_asyncio/test_streams.py +++ b/Lib/test/test_asyncio/test_streams.py @@ -592,7 +592,7 @@ async def handle_client(self, client_reader, client_writer): await client_writer.wait_closed() def start(self): - sock = socket.bind_socket(('127.0.0.1', 0)) + sock = socket.create_server(('127.0.0.1', 0)) self.server = self.loop.run_until_complete( asyncio.start_server(self.handle_client, sock=sock, @@ -604,7 +604,7 @@ def handle_client_callback(self, client_reader, client_writer): client_writer)) def start_callback(self): - sock = socket.bind_socket(('127.0.0.1', 0)) + sock = socket.create_server(('127.0.0.1', 0)) addr = sock.getsockname() sock.close() self.server = self.loop.run_until_complete( @@ -794,7 +794,7 @@ def test_drain_raises(self): def server(): # Runs in a separate thread. - with socket.bind_socket(('localhost', 0)) as sock: + with socket.create_server(('localhost', 0)) as sock: addr = sock.getsockname() q.put(addr) clt, _ = sock.accept() diff --git a/Lib/test/test_epoll.py b/Lib/test/test_epoll.py index 4f45cdd7a55eb5..5df4ebc7bb7520 100644 --- a/Lib/test/test_epoll.py +++ b/Lib/test/test_epoll.py @@ -41,7 +41,7 @@ class TestEPoll(unittest.TestCase): def setUp(self): - self.serverSocket = socket.bind_socket(('127.0.0.1', 0)) + self.serverSocket = socket.create_server(('127.0.0.1', 0)) self.connections = [self.serverSocket] def tearDown(self): diff --git a/Lib/test/test_ftplib.py b/Lib/test/test_ftplib.py index 5cb253a98a10e4..b0e46411a2e2bf 100644 --- a/Lib/test/test_ftplib.py +++ b/Lib/test/test_ftplib.py @@ -132,7 +132,7 @@ def cmd_port(self, arg): self.push('200 active data connection established') def cmd_pasv(self, arg): - with socket.bind_socket((self.socket.getsockname()[0], 0)) as sock: + with socket.create_server((self.socket.getsockname()[0], 0)) as sock: sock.settimeout(TIMEOUT) ip, port = sock.getsockname()[:2] ip = ip.replace('.', ','); p1 = port / 256; p2 = port % 256 @@ -148,8 +148,8 @@ def cmd_eprt(self, arg): self.push('200 active data connection established') def cmd_epsv(self, arg): - with socket.bind_socket((self.socket.getsockname()[0], 0), - family=socket.AF_INET6) as sock: + with socket.create_server((self.socket.getsockname()[0], 0), + family=socket.AF_INET6) as sock: sock.settimeout(TIMEOUT) port = sock.getsockname()[1] self.push('229 entering extended passive mode (|||%d|)' %port) diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py index 35dc1b81f906a8..31b12113106a3a 100644 --- a/Lib/test/test_httplib.py +++ b/Lib/test/test_httplib.py @@ -1116,7 +1116,7 @@ def test_read1_bound_content_length(self): def test_response_fileno(self): # Make sure fd returned by fileno is valid. - serv = socket.bind_socket((HOST, 0)) + serv = socket.create_server((HOST, 0)) self.addCleanup(serv.close) result = None diff --git a/Lib/test/test_kqueue.py b/Lib/test/test_kqueue.py index 4f41687bd1770b..998fd9d46496bb 100644 --- a/Lib/test/test_kqueue.py +++ b/Lib/test/test_kqueue.py @@ -110,7 +110,7 @@ def test_create_event(self): def test_queue_event(self): - serverSocket = socket.bind_socket(('127.0.0.1', 0)) + serverSocket = socket.create_server(('127.0.0.1', 0)) client = socket.socket() client.setblocking(False) try: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index c091bfeb6ac166..8a8290fb440969 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6054,83 +6054,83 @@ def test_new_tcp_flags(self): "New TCP flags were discovered. See bpo-32394 for more information") -class BindSocketTest(unittest.TestCase): +class CreateServerTest(unittest.TestCase): def test_address(self): port = support.find_unused_port() - with socket.bind_socket(("127.0.0.1", port)) as sock: + with socket.create_server(("127.0.0.1", port)) as sock: self.assertEqual(sock.getsockname()[0], "127.0.0.1") self.assertEqual(sock.getsockname()[1], port) def test_address_all_nics(self): # host in (None, "") == 'all NICs' - with socket.bind_socket(("", 0)) as sock: + with socket.create_server(("", 0)) as sock: self.assertEqual(sock.getsockname()[0], "0.0.0.0") - with socket.bind_socket((None, 0)) as sock: + with socket.create_server((None, 0)) as sock: self.assertEqual(sock.getsockname()[0], "0.0.0.0") if support.IPV6_ENABLED: - with socket.bind_socket(("", 0), family=socket.AF_INET6) as sock: + with socket.create_server(("", 0), family=socket.AF_INET6) as sock: self.assertEqual(sock.getsockname()[0], "::") - with socket.bind_socket((None, 0), family=socket.AF_INET6) as sock: + with socket.create_server((None, 0), family=socket.AF_INET6) as sock: self.assertEqual(sock.getsockname()[0], "::") def test_family(self): # determined by address - with socket.bind_socket(("127.0.0.1", 0)) as sock: + with socket.create_server(("127.0.0.1", 0)) as sock: self.assertEqual(sock.family, socket.AF_INET) if support.IPV6_ENABLED: - with socket.bind_socket(("::1", 0)) as sock: + with socket.create_server(("::1", 0)) as sock: self.assertEqual(sock.family, socket.AF_INET6) # determined by 'family' arg - with socket.bind_socket(("localhost", 0), - family=socket.AF_INET) as sock: + with socket.create_server(("localhost", 0), + family=socket.AF_INET) as sock: self.assertEqual(sock.family, socket.AF_INET) if support.IPV6_ENABLED: - with socket.bind_socket(("localhost", 0), - family=socket.AF_INET6) as sock: + with socket.create_server(("localhost", 0), + family=socket.AF_INET6) as sock: self.assertEqual(sock.family, socket.AF_INET6) def test_type(self): - with socket.bind_socket(("localhost", 0)) as sock: + with socket.create_server(("localhost", 0)) as sock: self.assertEqual(sock.type, socket.SOCK_STREAM) - with socket.bind_socket(("localhost", 0), - type=socket.SOCK_DGRAM) as sock: + with socket.create_server(("localhost", 0), + type=socket.SOCK_DGRAM) as sock: self.assertEqual(sock.type, socket.SOCK_DGRAM) with self.assertRaises(ValueError): - socket.bind_socket(("localhost", 0), type=0) + socket.create_server(("localhost", 0), type=0) def test_reuse_addr(self): if not hasattr(socket, "SO_REUSEADDR"): with self.assertRaises(ValueError, socket): - socket.bind_socket(("localhost", 0), reuse_addr=True) + socket.create_server(("localhost", 0), reuse_addr=True) return # check False - with socket.bind_socket(("localhost", 0), reuse_addr=False) as sock: + with socket.create_server(("localhost", 0), reuse_addr=False) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) self.assertEqual(opt, 0) port = sock.getsockname()[1] # Make sure the same port can be reused once socket is closed, # meaning SO_REUSEADDR is implicitly set by default. - with socket.bind_socket(("localhost", port)) as sock: + with socket.create_server(("localhost", port)) as sock: pass @unittest.skipIf(os.name not in ('nt', 'cygwin'), "Windows only") def test_reuse_addr_win(self): with self.assertRaises(ValueError): - socket.bind_socket(("localhost, 0"), reuse_addr=True) - s = socket.bind_socket(("localhost, 0"), reuse_addr=True, - type=socket.SOCK_DGRAM) + socket.create_server(("localhost, 0"), reuse_addr=True) + s = socket.create_server(("localhost, 0"), reuse_addr=True, + type=socket.SOCK_DGRAM) s.close() def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): with self.assertRaises(ValueError, socket.error): - socket.bind_socket(("localhost", 0), reuse_port=True) + socket.create_server(("localhost", 0), reuse_port=True) else: - with socket.bind_socket(("localhost", 0)) as sock: + with socket.create_server(("localhost", 0)) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) self.assertEqual(opt, 0) - with socket.bind_socket(("localhost", 0), reuse_port=True) as sock: + with socket.create_server(("localhost", 0), reuse_port=True) as sock: opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) self.assertNotEqual(opt, 0) @@ -6138,27 +6138,27 @@ def test_reuse_port(self): not hasattr(_socket, 'IPV6_V6ONLY'), "IPV6_V6ONLY option not supported") def test_ipv6only_default(self): - with socket.bind_socket(("::1", 0)) as sock: + with socket.create_server(("::1", 0)) as sock: assert sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) def test_ipv4_default(self): - with socket.bind_socket(("localhost", 0)) as sock: + with socket.create_server(("localhost", 0)) as sock: self.assertEqual(sock.family, socket.AF_INET) @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_hybrid_ipv6_default(self): - with socket.bind_socket(("localhost", 0), hybrid_ipv46=True) as sock: + with socket.create_server(("localhost", 0), hybrid_ipv46=True) as sock: self.assertEqual(sock.family, socket.AF_INET6) def test_dual_stack_udp(self): # Hybrid IPv4/6 UDP servers won't allow IPv4 connections. with self.assertRaises(ValueError): - socket.bind_socket(("localhost", 0), type=socket.SOCK_DGRAM, - hybrid_ipv46=True) + socket.create_server(("localhost", 0), type=socket.SOCK_DGRAM, + hybrid_ipv46=True) -class BindSocketFunctionalTest(unittest.TestCase): +class CreateServerFunctionalTest(unittest.TestCase): timeout = 3 def setUp(self): @@ -6207,25 +6207,25 @@ def echo_client(self, sock, connect_host=None): self.assertEqual(msg, b'foo') def test_tcp4(self): - with socket.bind_socket(("localhost", 0), family=socket.AF_INET, - type=socket.SOCK_STREAM) as sock: + with socket.create_server(("localhost", 0), family=socket.AF_INET, + type=socket.SOCK_STREAM) as sock: self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): - with socket.bind_socket(("localhost", 0), family=socket.AF_INET6, - type=socket.SOCK_STREAM) as sock: + with socket.create_server(("localhost", 0), family=socket.AF_INET6, + type=socket.SOCK_STREAM) as sock: self.echo_client(sock) def test_udp4(self): - with socket.bind_socket(("localhost", 0), family=socket.AF_INET, - type=socket.SOCK_DGRAM) as sock: + with socket.create_server(("localhost", 0), family=socket.AF_INET, + type=socket.SOCK_DGRAM) as sock: self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_udp6(self): - with socket.bind_socket(("localhost", 0), family=socket.AF_INET6, - type=socket.SOCK_DGRAM) as sock: + with socket.create_server(("localhost", 0), family=socket.AF_INET6, + type=socket.SOCK_DGRAM) as sock: self.echo_client(sock) # --- @@ -6236,20 +6236,20 @@ def test_udp6(self): @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_dual_stack_tcp4(self): - with socket.bind_socket((None, 0), hybrid_ipv46=True) as sock: + with socket.create_server((None, 0), hybrid_ipv46=True) as sock: self.echo_client(sock, connect_host="127.0.0.1") @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_dual_stack_tcp6(self): - with socket.bind_socket((None, 0), hybrid_ipv46=True) as sock: + with socket.create_server((None, 0), hybrid_ipv46=True) as sock: self.echo_client(sock, connect_host="::1") def test_main(): tests = [GeneralModuleTests, BasicTCPTest, TCPCloserTest, TCPTimeoutTest, TestExceptions, BufferIOTest, BasicTCPTest2, BasicUDPTest, - UDPTimeoutTest, BindSocketTest, BindSocketFunctionalTest] + UDPTimeoutTest, CreateServerTest, CreateServerFunctionalTest] tests.extend([ NonBlockingTCPTests, diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 52ccef6d1d5261..28e6ffecedd0f8 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -752,7 +752,7 @@ def test_server_side(self): def test_unknown_channel_binding(self): # should raise ValueError for unknown type - s = socket.bind_socket(('127.0.0.1', 0)) + s = socket.create_server(('127.0.0.1', 0)) c = socket.socket(socket.AF_INET) c.connect(s.getsockname()) with test_wrap_socket(c, do_handshake_on_connect=False) as ss: @@ -1644,7 +1644,7 @@ def test_subclass(self): ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE - with socket.bind_socket(("127.0.0.1", 0)) as s: + with socket.create_server(("127.0.0.1", 0)) as s: c = socket.create_connection(s.getsockname()) c.setblocking(False) with ctx.wrap_socket(c, False, do_handshake_on_connect=False) as c: diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index 345622d186d4cc..cb664bab17109d 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -91,12 +91,12 @@ def test_forget(self): support.rmtree('__pycache__') def test_HOST(self): - s = socket.bind_socket((support.HOST, 0)) + s = socket.create_server((support.HOST, 0)) s.close() def test_find_unused_port(self): port = support.find_unused_port() - s = socket.bind_socket((support.HOST, port)) + s = socket.create_server((support.HOST, port)) s.close() def test_bind_port(self): diff --git a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst index 62c3c25aa1372f..e7fa704211575c 100644 --- a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst +++ b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst @@ -1,4 +1,4 @@ -Added :meth:`~socket.bind_socket()` and :meth:`~socket.supports_hybrid_ipv46()` +Added :meth:`~socket.create_server()` and :meth:`~socket.supports_hybrid_ipv46()` convenience functions to automatize the necessary tasks usually involved when creating a server socket, including accepting both IPv4 and IPv6 connections on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) From 9cd6f01c6cbdc439e30e8ed192e1e03b8c86e450 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 12:54:56 +0100 Subject: [PATCH 27/43] get rid of reuse_addr arg --- Doc/library/socket.rst | 5 ++--- Lib/socket.py | 22 ++++++---------------- Lib/test/test_socket.py | 23 ----------------------- 3 files changed, 8 insertions(+), 42 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 4c56d79a4aa213..bb58e0d31510ab 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,7 +595,7 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_addr=None, reuse_port=False, flags=None, hybrid_ipv46=False) +.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_port=False, flags=None, hybrid_ipv46=False) Convenience function which aims at automating all the typical steps needed when creating a server socket. @@ -613,8 +613,7 @@ The following functions all create :ref:`socket objects `. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. *backlog* is the queue size passed to :meth:`socket.listen` if :data:`SOCK_STREAM` *type* is used. - *reuse_addr* and *reuse_port* dictates whether to use :data:`SO_REUSEADDR` - and :data:`SO_REUSEPORT` socket options respectively. + *reuse_port* dictates whether to set :data:`SO_REUSEPORT` socket option. *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. diff --git a/Lib/socket.py b/Lib/socket.py index 8333ebe6b9c332..303cde97ea446f 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -744,8 +744,7 @@ def supports_hybrid_ipv46(): return False def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, - reuse_addr=None, reuse_port=False, flags=None, - hybrid_ipv46=False): + reuse_port=False, flags=None, hybrid_ipv46=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -760,8 +759,7 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, *backlog* is the queue size passed to socket.listen() and is ignored for SOCK_DGRAM socket types. - *reuse_addr* and *reuse_port* dictate whether to use SO_REUSEADDR - and SO_REUSEPORT socket options. + *reuse_port* dictate whether to use the SO_REUSEPORT socket option. *flags* is a bitmask for getaddrinfo(). @@ -779,24 +777,17 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, if host == "": # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html host = None # all interfaces - # Note about Windows: by default SO_REUSEADDR is not set because: + # Note about Windows: we don't set SO_REUSEADDR because: # 1) It's unnecessary: bind() will succeed even in case of a # previous closed socket on the same address and still in TIME_WAIT # state. # 2) If set, another socket will be free to bind() on the same # address, effectively preventing this one from accepting connections. # Also, it sets the process in a state where it'll no longer respond - # to any signals or graceful kills. The option can still be set though - # for SOCK_DGRAM multicast sockets. + # to any signals or graceful kills. More at: # See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx - iswin = os.name in ('nt', 'cygwin') - if reuse_addr is None: - reuse_addr = not iswin and hasattr(_socket, 'SO_REUSEADDR') - elif reuse_addr and not hasattr(_socket, 'SO_REUSEADDR'): - raise ValueError("SO_REUSEADDR not supported on this platform") - elif reuse_addr and iswin and type != SOCK_DGRAM: - raise ValueError("SO_REUSEADDR allowed for SOCK_DGRAM type only") - + reuse_addr = os.name not in ('nt', 'cygwin') and \ + hasattr(_socket, 'SO_REUSEADDR') if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") if type not in (SOCK_STREAM, SOCK_DGRAM): @@ -811,7 +802,6 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, if not supports_hybrid_ipv46(): raise ValueError("hybrid_ipv46 not supported on this platform") family = AF_INET6 - info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: # prefer AF_INET over AF_INET6 diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 8a8290fb440969..415855a878a80b 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6099,29 +6099,6 @@ def test_type(self): with self.assertRaises(ValueError): socket.create_server(("localhost", 0), type=0) - def test_reuse_addr(self): - if not hasattr(socket, "SO_REUSEADDR"): - with self.assertRaises(ValueError, socket): - socket.create_server(("localhost", 0), reuse_addr=True) - return - # check False - with socket.create_server(("localhost", 0), reuse_addr=False) as sock: - opt = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) - self.assertEqual(opt, 0) - port = sock.getsockname()[1] - # Make sure the same port can be reused once socket is closed, - # meaning SO_REUSEADDR is implicitly set by default. - with socket.create_server(("localhost", port)) as sock: - pass - - @unittest.skipIf(os.name not in ('nt', 'cygwin'), "Windows only") - def test_reuse_addr_win(self): - with self.assertRaises(ValueError): - socket.create_server(("localhost, 0"), reuse_addr=True) - s = socket.create_server(("localhost, 0"), reuse_addr=True, - type=socket.SOCK_DGRAM) - s.close() - def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): with self.assertRaises(ValueError, socket.error): From d0e69bb1285cbddec2c62a608ce91936641074c6 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 13:10:57 +0100 Subject: [PATCH 28/43] address @vstinner comments --- Doc/library/socket.rst | 15 ++++++++++----- Lib/socket.py | 11 ++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index bb58e0d31510ab..09b063ffeb4597 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,25 +595,30 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, reuse_port=False, flags=None, hybrid_ipv46=False) +.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, reuse_port=False, flags=None, hybrid_ipv46=False) Convenience function which aims at automating all the typical steps needed when creating a server socket. It creates a socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object upon which you can call :meth:`socket.accept()` in - order to accept new connections. + order to accept new connections.Internally it relies on :meth:`getaddrinfo()` + and returns the first socket which can be bound to *address*. - Internally it relies on :meth:`getaddrinfo()` and returns the first socket - which can be bound to *address*. If *host* is an empty string or ``None`` all network interfaces are assumed. + If *family* is :data:`AF_UNSPEC` the address family will be determined from the *host* specified in *address*. If family can't clearly be determined from *host* and *hybrid_ipv46* is ``False`` then :data:`AF_INET` family will be preferred over :data:`AF_INET6`. + *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. + *backlog* is the queue size passed to :meth:`socket.listen` if - :data:`SOCK_STREAM` *type* is used. + :data:`SOCK_STREAM` *type* is used. If left ``None`` a default reasonable + value is chosen. + *reuse_port* dictates whether to set :data:`SO_REUSEPORT` socket option. + *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. diff --git a/Lib/socket.py b/Lib/socket.py index 303cde97ea446f..3bf5146dcd73f2 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -743,7 +743,7 @@ def supports_hybrid_ipv46(): except error: return False -def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, +def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, reuse_port=False, flags=None, hybrid_ipv46=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -775,6 +775,7 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, """ host, port = address if host == "": + # When using getaddrinfo(), None is an alias for "all interfaces": # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html host = None # all interfaces # Note about Windows: we don't set SO_REUSEADDR because: @@ -823,20 +824,20 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, try: sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) except error: - # Fail later on on bind(), for platforms which may not + # Fail later on bind(), for platforms which may not # support this option. pass if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) if has_ipv6 and af == AF_INET6: - if not hybrid_ipv46: + if not hybrid_ipv46 and hasattr(_socket, "IPV6_V6ONLY"): # Disable IPv4/IPv6 dual stack support (enabled by # default on Linux) which makes a single socket # listen on both address families in order to be # consistent across different platforms. try: sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) - except NameError: + except error: pass else: sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) @@ -847,7 +848,7 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=128, (err.strerror, sa) raise error(err.errno, es) from None if socktype == SOCK_STREAM: - sock.listen(backlog) + sock.listen(backlog or 0) # Break explicitly a reference cycle. err = None return sock From f93058b9e8c9dcdb4e002ffba3dc4ccfacaa12d5 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 15:38:33 +0100 Subject: [PATCH 29/43] fix NameError + add comment clarifying why we in case of ambiguous host we prever AF_INET over AF_INET6 --- Doc/library/socket.rst | 6 +++--- Lib/socket.py | 14 ++++++++++---- Lib/test/test_socket.py | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 09b063ffeb4597..790f918fafe6c9 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -608,7 +608,7 @@ The following functions all create :ref:`socket objects `. If *family* is :data:`AF_UNSPEC` the address family will be determined from the *host* specified in *address*. If family can't clearly be determined - from *host* and *hybrid_ipv46* is ``False`` then :data:`AF_INET` family will + from *host* and *hybrid_ipv46* is false then :data:`AF_INET` family will be preferred over :data:`AF_INET6`. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. @@ -622,12 +622,12 @@ The following functions all create :ref:`socket objects `. *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. - If *hybrid_ipv46* is ``True`` and the platform supports it the socket will + If *hybrid_ipv46* is tre and the platform supports it the socket will be able to accept both IPv4 and IPv6 connections. In this case the address returned by :meth:`socket.getpeername` when a new IPv4 connection is accepted will be an IPv6 address represented as an IPv4-mapped IPv6 address like ``":ffff:127.0.0.1"``. - If *hybrid_ipv46* is ``False`` it will explicitly disable this option on + If *hybrid_ipv46* is false it will explicitly disable this option on platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this `MultipleSocketsListener recipe `__. diff --git a/Lib/socket.py b/Lib/socket.py index 3bf5146dcd73f2..63913be043c21a 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -805,7 +805,11 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, family = AF_INET6 info = getaddrinfo(host, port, family, type, 0, flags) if family == AF_UNSPEC: - # prefer AF_INET over AF_INET6 + # Prefer AF_INET over AF_INET6. Rationale: + # 1) AF_INET is the default for socket.socket() + # 2) in case of unambiguous host (None or "localhost") + # getaddrinfo() sorting order is not guaranteed, so we take a + # stance in order to eliminate cross-platform differences. info.sort(key=lambda x: x[0] == AF_INET, reverse=True) err = None @@ -830,11 +834,13 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) if has_ipv6 and af == AF_INET6: - if not hybrid_ipv46 and hasattr(_socket, "IPV6_V6ONLY"): + if not hybrid_ipv46 and \ + hasattr(_socket, "IPV6_V6ONLY") and \ + hasattr(_socket, "IPPROTO_IPV6"): # Disable IPv4/IPv6 dual stack support (enabled by # default on Linux) which makes a single socket - # listen on both address families in order to be - # consistent across different platforms. + # listen on both address families in order to + # eliminate cross-platform differences. try: sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) except error: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 415855a878a80b..cb64bf168a246d 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6101,7 +6101,7 @@ def test_type(self): def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): - with self.assertRaises(ValueError, socket.error): + with self.assertRaises(ValueError): socket.create_server(("localhost", 0), reuse_port=True) else: with socket.create_server(("localhost", 0)) as sock: From 63d762e5abc1b508646b8d4f35e2c4fd9eea9c0b Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 16:10:56 +0100 Subject: [PATCH 30/43] fix test failures --- Lib/socket.py | 11 +++++------ Lib/test/test_socket.py | 8 ++++---- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index 63913be043c21a..01527a14033542 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -786,7 +786,7 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, # address, effectively preventing this one from accepting connections. # Also, it sets the process in a state where it'll no longer respond # to any signals or graceful kills. More at: - # See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx + # msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx reuse_addr = os.name not in ('nt', 'cygwin') and \ hasattr(_socket, 'SO_REUSEADDR') if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): @@ -807,7 +807,7 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, if family == AF_UNSPEC: # Prefer AF_INET over AF_INET6. Rationale: # 1) AF_INET is the default for socket.socket() - # 2) in case of unambiguous host (None or "localhost") + # 2) in case of ambiguous host (None or "localhost") # getaddrinfo() sorting order is not guaranteed, so we take a # stance in order to eliminate cross-platform differences. info.sort(key=lambda x: x[0] == AF_INET, reverse=True) @@ -850,9 +850,9 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, try: sock.bind(sa) except error as err: - es = '%s (while attempting to bind on address %r)' % \ + err_msg = '%s (while attempting to bind on address %r)' % \ (err.strerror, sa) - raise error(err.errno, es) from None + raise error(err.errno, err_msg) from None if socktype == SOCK_STREAM: sock.listen(backlog or 0) # Break explicitly a reference cycle. @@ -864,8 +864,7 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, if err is not None: raise err - else: - raise error("getaddrinfo returns an empty list") + raise error("getaddrinfo returns an empty list") def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index cb64bf168a246d..8f4f12c9ac876b 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6086,7 +6086,7 @@ def test_family(self): family=socket.AF_INET) as sock: self.assertEqual(sock.family, socket.AF_INET) if support.IPV6_ENABLED: - with socket.create_server(("localhost", 0), + with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: self.assertEqual(sock.family, socket.AF_INET6) @@ -6125,7 +6125,7 @@ def test_ipv4_default(self): @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_hybrid_ipv6_default(self): - with socket.create_server(("localhost", 0), hybrid_ipv46=True) as sock: + with socket.create_server(("::1", 0), hybrid_ipv46=True) as sock: self.assertEqual(sock.family, socket.AF_INET6) def test_dual_stack_udp(self): @@ -6190,7 +6190,7 @@ def test_tcp4(self): @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): - with socket.create_server(("localhost", 0), family=socket.AF_INET6, + with socket.create_server(("::1", 0), family=socket.AF_INET6, type=socket.SOCK_STREAM) as sock: self.echo_client(sock) @@ -6201,7 +6201,7 @@ def test_udp4(self): @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_udp6(self): - with socket.create_server(("localhost", 0), family=socket.AF_INET6, + with socket.create_server(("::1", 0), family=socket.AF_INET6, type=socket.SOCK_DGRAM) as sock: self.echo_client(sock) From e95da5992a511bfa4c6f176cd438286fcbc90389 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 23:31:47 +0100 Subject: [PATCH 31/43] don't use getaddrinfo(), change function signature https://github.com/python/cpython/pull/11784#issuecomment-463736515 --- Lib/socket.py | 143 +++++++++++++--------------------------- Lib/test/test_socket.py | 89 ++++++------------------- 2 files changed, 66 insertions(+), 166 deletions(-) diff --git a/Lib/socket.py b/Lib/socket.py index 01527a14033542..c1a75f9f4a7000 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -743,26 +743,17 @@ def supports_hybrid_ipv46(): except error: return False -def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, - reuse_port=False, flags=None, hybrid_ipv46=False): +def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, + hybrid_ipv46=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. - If *host* is an empty string or None all network interfaces are assumed. + *family* should be either AF_INET or AF_INET6. - If *family* is AF_UNSPEC the address family will be determined from the - *host* specified in *address* and AF_INET will take precedence over - AF_INET6 in case family can't be determined from *host*. - - *type* should be either SOCK_STREAM or SOCK_DGRAM. - - *backlog* is the queue size passed to socket.listen() and is ignored - for SOCK_DGRAM socket types. + *backlog* is the queue size passed to socket.listen(). *reuse_port* dictate whether to use the SO_REUSEPORT socket option. - *flags* is a bitmask for getaddrinfo(). - *hybrid_ipv46*: if True and the platform supports it it will create a socket able to accept both IPv4 and IPv6 connections. When False it will explicitly disable this option on platforms that @@ -773,98 +764,54 @@ def create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, ... conn, addr = server.accept() ... # handle new connection """ - host, port = address - if host == "": - # When using getaddrinfo(), None is an alias for "all interfaces": - # https://mail.python.org/pipermail/python-ideas/2013-March/019937.html - host = None # all interfaces - # Note about Windows: we don't set SO_REUSEADDR because: - # 1) It's unnecessary: bind() will succeed even in case of a - # previous closed socket on the same address and still in TIME_WAIT - # state. - # 2) If set, another socket will be free to bind() on the same - # address, effectively preventing this one from accepting connections. - # Also, it sets the process in a state where it'll no longer respond - # to any signals or graceful kills. More at: - # msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx - reuse_addr = os.name not in ('nt', 'cygwin') and \ - hasattr(_socket, 'SO_REUSEADDR') if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") - if type not in (SOCK_STREAM, SOCK_DGRAM): - raise ValueError("only SOCK_STREAM and SOCK_DGRAM types are supported") - if flags is None: - flags = AI_PASSIVE if hybrid_ipv46: - if type != SOCK_STREAM: - raise ValueError("hybrid_ipv46 requires SOCK_STREAM type") - if family not in {AF_INET6, AF_UNSPEC}: - raise ValueError("hybrid_ipv46 requires AF_INET6 family") if not supports_hybrid_ipv46(): raise ValueError("hybrid_ipv46 not supported on this platform") - family = AF_INET6 - info = getaddrinfo(host, port, family, type, 0, flags) - if family == AF_UNSPEC: - # Prefer AF_INET over AF_INET6. Rationale: - # 1) AF_INET is the default for socket.socket() - # 2) in case of ambiguous host (None or "localhost") - # getaddrinfo() sorting order is not guaranteed, so we take a - # stance in order to eliminate cross-platform differences. - info.sort(key=lambda x: x[0] == AF_INET, reverse=True) - - err = None - for res in info: - af, socktype, proto, canonname, sa = res - try: - sock = socket(af, socktype, proto) - except error as _err: - err = _err - if err.errno == errno.EAFNOSUPPORT: - continue + sock = socket(family, SOCK_STREAM) + try: + # Note about Windows: we don't set SO_REUSEADDR because: + # 1) It's unnecessary: bind() will succeed even in case of a + # previous closed socket on the same address and still in + # TIME_WAIT state. + # 2) If set, another socket will be free to bind() on the same + # address, effectively preventing this one from accepting + # connections. Also, it sets the process in a state where it'll + # no longer respond to any signals or graceful kills. More at: + # msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx + if os.name not in ('nt', 'cygwin') and \ + hasattr(_socket, 'SO_REUSEADDR'): + try: + sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) + except error: + # Fail later on bind(), for platforms which may not + # support this option. + pass + if reuse_port: + sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) + if has_ipv6 and family == AF_INET6: + if not hybrid_ipv46 and \ + hasattr(_socket, "IPV6_V6ONLY") and \ + hasattr(_socket, "IPPROTO_IPV6"): + # Disable IPv4/IPv6 dual stack support (enabled by + # default on Linux) which makes a single socket + # listen on both address families in order to + # eliminate cross-platform differences. + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) else: - raise + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) try: - if reuse_addr: - try: - sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - except error: - # Fail later on bind(), for platforms which may not - # support this option. - pass - if reuse_port: - sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) - if has_ipv6 and af == AF_INET6: - if not hybrid_ipv46 and \ - hasattr(_socket, "IPV6_V6ONLY") and \ - hasattr(_socket, "IPPROTO_IPV6"): - # Disable IPv4/IPv6 dual stack support (enabled by - # default on Linux) which makes a single socket - # listen on both address families in order to - # eliminate cross-platform differences. - try: - sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) - except error: - pass - else: - sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) - try: - sock.bind(sa) - except error as err: - err_msg = '%s (while attempting to bind on address %r)' % \ - (err.strerror, sa) - raise error(err.errno, err_msg) from None - if socktype == SOCK_STREAM: - sock.listen(backlog or 0) - # Break explicitly a reference cycle. - err = None - return sock - except error: - sock.close() - raise - - if err is not None: - raise err - raise error("getaddrinfo returns an empty list") + sock.bind(address) + except error as err: + err_msg = '%s (while attempting to bind on address %r)' % \ + (err.strerror, address) + raise error(err.errno, err_msg) from None + sock.listen(backlog or 0) + return sock + except Exception: + sock.close() + raise def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 8f4f12c9ac876b..17c92cdf683ab4 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6061,43 +6061,20 @@ def test_address(self): with socket.create_server(("127.0.0.1", port)) as sock: self.assertEqual(sock.getsockname()[0], "127.0.0.1") self.assertEqual(sock.getsockname()[1], port) - - def test_address_all_nics(self): - # host in (None, "") == 'all NICs' - with socket.create_server(("", 0)) as sock: - self.assertEqual(sock.getsockname()[0], "0.0.0.0") - with socket.create_server((None, 0)) as sock: - self.assertEqual(sock.getsockname()[0], "0.0.0.0") if support.IPV6_ENABLED: - with socket.create_server(("", 0), family=socket.AF_INET6) as sock: - self.assertEqual(sock.getsockname()[0], "::") - with socket.create_server((None, 0), family=socket.AF_INET6) as sock: - self.assertEqual(sock.getsockname()[0], "::") + with socket.create_server(("::1", port), + family=socket.AF_INET6) as sock: + self.assertEqual(sock.getsockname()[0], "::1") + self.assertEqual(sock.getsockname()[1], port) - def test_family(self): - # determined by address + def test_family_and_type(self): with socket.create_server(("127.0.0.1", 0)) as sock: self.assertEqual(sock.family, socket.AF_INET) - if support.IPV6_ENABLED: - with socket.create_server(("::1", 0)) as sock: - self.assertEqual(sock.family, socket.AF_INET6) - # determined by 'family' arg - with socket.create_server(("localhost", 0), - family=socket.AF_INET) as sock: - self.assertEqual(sock.family, socket.AF_INET) - if support.IPV6_ENABLED: - with socket.create_server(("::1", 0), - family=socket.AF_INET6) as sock: - self.assertEqual(sock.family, socket.AF_INET6) - - def test_type(self): - with socket.create_server(("localhost", 0)) as sock: self.assertEqual(sock.type, socket.SOCK_STREAM) - with socket.create_server(("localhost", 0), - type=socket.SOCK_DGRAM) as sock: - self.assertEqual(sock.type, socket.SOCK_DGRAM) - with self.assertRaises(ValueError): - socket.create_server(("localhost", 0), type=0) + if support.IPV6_ENABLED: + with socket.create_server(("::1", 0), family=socket.AF_INET6) as s: + self.assertEqual(s.family, socket.AF_INET6) + self.assertEqual(sock.type, socket.SOCK_STREAM) def test_reuse_port(self): if not hasattr(socket, "SO_REUSEPORT"): @@ -6114,26 +6091,17 @@ def test_reuse_port(self): @unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or not hasattr(_socket, 'IPV6_V6ONLY'), "IPV6_V6ONLY option not supported") - def test_ipv6only_default(self): - with socket.create_server(("::1", 0)) as sock: + def test_ipv6_only_default(self): + with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: assert sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) - def test_ipv4_default(self): - with socket.create_server(("localhost", 0)) as sock: - self.assertEqual(sock.family, socket.AF_INET) - @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") - def test_hybrid_ipv6_default(self): - with socket.create_server(("::1", 0), hybrid_ipv46=True) as sock: + def test_hybrid_ipv6_family(self): + with socket.create_server(("::1", 0), family=socket.AF_INET6, + hybrid_ipv46=True) as sock: self.assertEqual(sock.family, socket.AF_INET6) - def test_dual_stack_udp(self): - # Hybrid IPv4/6 UDP servers won't allow IPv4 connections. - with self.assertRaises(ValueError): - socket.create_server(("localhost", 0), type=socket.SOCK_DGRAM, - hybrid_ipv46=True) - class CreateServerFunctionalTest(unittest.TestCase): timeout = 3 @@ -6184,42 +6152,27 @@ def echo_client(self, sock, connect_host=None): self.assertEqual(msg, b'foo') def test_tcp4(self): - with socket.create_server(("localhost", 0), family=socket.AF_INET, - type=socket.SOCK_STREAM) as sock: + with socket.create_server(("localhost", 0), + family=socket.AF_INET) as sock: self.echo_client(sock) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): - with socket.create_server(("::1", 0), family=socket.AF_INET6, - type=socket.SOCK_STREAM) as sock: - self.echo_client(sock) - - def test_udp4(self): - with socket.create_server(("localhost", 0), family=socket.AF_INET, - type=socket.SOCK_DGRAM) as sock: - self.echo_client(sock) - - @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') - def test_udp6(self): - with socket.create_server(("::1", 0), family=socket.AF_INET6, - type=socket.SOCK_DGRAM) as sock: + with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: self.echo_client(sock) - # --- - # Dual-stack IPv4/6 tests: create a hybrid IPv4/6 server and - # connect with a client using IPv4 and IPv6 addresses. - # --- - @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_dual_stack_tcp4(self): - with socket.create_server((None, 0), hybrid_ipv46=True) as sock: + with socket.create_server(("", 0), family=socket.AF_INET6, + hybrid_ipv46=True) as sock: self.echo_client(sock, connect_host="127.0.0.1") @unittest.skipIf(not socket.supports_hybrid_ipv46(), "hybrid_ipv46 not supported") def test_dual_stack_tcp6(self): - with socket.create_server((None, 0), hybrid_ipv46=True) as sock: + with socket.create_server(("", 0), family=socket.AF_INET6, + hybrid_ipv46=True) as sock: self.echo_client(sock, connect_host="::1") From 22ea974555710d1e89f4ec036bfd8f6fc5d57309 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 14 Feb 2019 23:39:31 +0100 Subject: [PATCH 32/43] rename arg hybrid_ipv46 -> dualstack_ipv6 --- Doc/library/socket.rst | 16 ++++++++-------- Doc/whatsnew/3.8.rst | 2 +- Lib/socket.py | 18 ++++++++++-------- Lib/test/test_socket.py | 18 +++++++++--------- .../2019-02-07-20-25-39.bpo-35934.QmfNmY.rst | 2 +- 5 files changed, 29 insertions(+), 27 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 790f918fafe6c9..0b2dccc1b5e4ab 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,7 +595,7 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, reuse_port=False, flags=None, hybrid_ipv46=False) +.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, reuse_port=False, flags=None, dualstack_ipv6=False) Convenience function which aims at automating all the typical steps needed when creating a server socket. @@ -608,7 +608,7 @@ The following functions all create :ref:`socket objects `. If *family* is :data:`AF_UNSPEC` the address family will be determined from the *host* specified in *address*. If family can't clearly be determined - from *host* and *hybrid_ipv46* is false then :data:`AF_INET` family will + from *host* and *dualstack_ipv6* is false then :data:`AF_INET` family will be preferred over :data:`AF_INET6`. *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. @@ -622,16 +622,16 @@ The following functions all create :ref:`socket objects `. *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` :data:`AI_PASSIVE` is used. - If *hybrid_ipv46* is tre and the platform supports it the socket will + If *dualstack_ipv6* is tre and the platform supports it the socket will be able to accept both IPv4 and IPv6 connections. In this case the address returned by :meth:`socket.getpeername` when a new IPv4 connection is accepted will be an IPv6 address represented as an IPv4-mapped IPv6 address like ``":ffff:127.0.0.1"``. - If *hybrid_ipv46* is false it will explicitly disable this option on + If *dualstack_ipv6* is false it will explicitly disable this option on platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this `MultipleSocketsListener recipe `__. - This parameter can be used in conjunction with :func:`supports_hybrid_ipv46`. + This parameter can be used in conjunction with :func:`has_dualstack_ipv6`. Here's an echo server example listening on all interfaces, port 8888, and (possibly) hybrid IPv4/IPv6 support: @@ -641,7 +641,7 @@ The following functions all create :ref:`socket objects `. import socket with socket.create_server(("", 8888), - hybrid_ipv46=socket.supports_hybrid_ipv46()) as server: + dualstack_ipv6=socket.has_dualstack_ipv6()) as server: conn, addr = server.accept() with conn: while True: @@ -652,7 +652,7 @@ The following functions all create :ref:`socket objects `. .. versionadded:: 3.8 -.. function:: supports_hybrid_ipv46() +.. function:: has_dualstack_ipv6() Return ``True`` if the platform supports creating a single :data:`SOCK_STREAM` socket which can accept both IPv4 and IPv6 connections. @@ -1854,7 +1854,7 @@ IPv4/IPv6 server on platforms supporting this functionality. HOST = None PORT = 50007 - s = socket.create_server((HOST, PORT), hybrid_ipv46=socket.supports_hybrid_ipv46()) + s = socket.create_server((HOST, PORT), dualstack_ipv6=socket.has_dualstack_ipv6()) conn, addr = s.accept() with conn: print('Connected by', addr) diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index a0452ee7561125..06062c5f657039 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -217,7 +217,7 @@ contain characters unrepresentable at the OS level. socket ------ -Added :meth:`~socket.create_server()` and :meth:`~socket.supports_hybrid_ipv46()` +Added :meth:`~socket.create_server()` and :meth:`~socket.has_dualstack_ipv6()` convenience functions to automatize the necessary tasks usually involved when creating a server socket, including accepting both IPv4 and IPv6 connections on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) diff --git a/Lib/socket.py b/Lib/socket.py index c1a75f9f4a7000..7fde96454daa11 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -61,7 +61,7 @@ EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11) __all__ = ["fromfd", "getfqdn", "create_connection", "create_server", - "supports_hybrid_ipv46", "AddressFamily", "SocketKind"] + "has_dualstack_ipv6", "AddressFamily", "SocketKind"] __all__.extend(os._get_exports_list(_socket)) # Set up the socket.AF_* socket.SOCK_* constants as members of IntEnums for @@ -728,7 +728,8 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, else: raise error("getaddrinfo returns an empty list") -def supports_hybrid_ipv46(): + +def has_dualstack_ipv6(): """Return True if the platform supports creating a SOCK_STREAM socket which can handle both AF_INET and AF_INET6 (IPv4 / IPv6) connections. """ @@ -743,8 +744,9 @@ def supports_hybrid_ipv46(): except error: return False + def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, - hybrid_ipv46=False): + dualstack_ipv6=False): """Convenience function which creates a socket bound to *address* (a 2-tuple (host, port)) and return the socket object. @@ -754,7 +756,7 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, *reuse_port* dictate whether to use the SO_REUSEPORT socket option. - *hybrid_ipv46*: if True and the platform supports it it will create + *dualstack_ipv6*: if True and the platform supports it it will create a socket able to accept both IPv4 and IPv6 connections. When False it will explicitly disable this option on platforms that enable it by default (e.g. Linux). @@ -766,9 +768,9 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, """ if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): raise ValueError("SO_REUSEPORT not supported on this platform") - if hybrid_ipv46: - if not supports_hybrid_ipv46(): - raise ValueError("hybrid_ipv46 not supported on this platform") + if dualstack_ipv6: + if not has_dualstack_ipv6(): + raise ValueError("dualstack_ipv6 not supported on this platform") sock = socket(family, SOCK_STREAM) try: # Note about Windows: we don't set SO_REUSEADDR because: @@ -791,7 +793,7 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) if has_ipv6 and family == AF_INET6: - if not hybrid_ipv46 and \ + if not dualstack_ipv6 and \ hasattr(_socket, "IPV6_V6ONLY") and \ hasattr(_socket, "IPPROTO_IPV6"): # Disable IPv4/IPv6 dual stack support (enabled by diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 17c92cdf683ab4..c1e95a1fd724f2 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6095,11 +6095,11 @@ def test_ipv6_only_default(self): with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: assert sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) - @unittest.skipIf(not socket.supports_hybrid_ipv46(), - "hybrid_ipv46 not supported") + @unittest.skipIf(not socket.has_dualstack_ipv6(), + "dualstack_ipv6 not supported") def test_hybrid_ipv6_family(self): with socket.create_server(("::1", 0), family=socket.AF_INET6, - hybrid_ipv46=True) as sock: + dualstack_ipv6=True) as sock: self.assertEqual(sock.family, socket.AF_INET6) @@ -6161,18 +6161,18 @@ def test_tcp6(self): with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: self.echo_client(sock) - @unittest.skipIf(not socket.supports_hybrid_ipv46(), - "hybrid_ipv46 not supported") + @unittest.skipIf(not socket.has_dualstack_ipv6(), + "dualstack_ipv6 not supported") def test_dual_stack_tcp4(self): with socket.create_server(("", 0), family=socket.AF_INET6, - hybrid_ipv46=True) as sock: + dualstack_ipv6=True) as sock: self.echo_client(sock, connect_host="127.0.0.1") - @unittest.skipIf(not socket.supports_hybrid_ipv46(), - "hybrid_ipv46 not supported") + @unittest.skipIf(not socket.has_dualstack_ipv6(), + "dualstack_ipv6 not supported") def test_dual_stack_tcp6(self): with socket.create_server(("", 0), family=socket.AF_INET6, - hybrid_ipv46=True) as sock: + dualstack_ipv6=True) as sock: self.echo_client(sock, connect_host="::1") diff --git a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst index e7fa704211575c..b10dff03d17434 100644 --- a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst +++ b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst @@ -1,4 +1,4 @@ -Added :meth:`~socket.create_server()` and :meth:`~socket.supports_hybrid_ipv46()` +Added :meth:`~socket.create_server()` and :meth:`~socket.has_dualstack_ipv6()` convenience functions to automatize the necessary tasks usually involved when creating a server socket, including accepting both IPv4 and IPv6 connections on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) From ad8a2196e10614c8b02da391268915135cdd6382 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 00:52:40 +0100 Subject: [PATCH 33/43] update doc --- Doc/library/socket.rst | 59 +++++++++-------------- Lib/socket.py | 33 ++++++------- Lib/test/test_asyncio/test_base_events.py | 3 +- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 0b2dccc1b5e4ab..28ab8f630d9549 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,53 +595,36 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: create_server(address, *, family=AF_UNSPEC, type=SOCK_STREAM, backlog=None, reuse_port=False, flags=None, dualstack_ipv6=False) +.. function:: create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False) - Convenience function which aims at automating all the typical steps needed - when creating a server socket. - It creates a socket bound to *address* (a 2-tuple ``(host, port)``) and - return the socket object upon which you can call :meth:`socket.accept()` in - order to accept new connections.Internally it relies on :meth:`getaddrinfo()` - and returns the first socket which can be bound to *address*. - - If *host* is an empty string or ``None`` all network interfaces are assumed. - - If *family* is :data:`AF_UNSPEC` the address family will be determined from - the *host* specified in *address*. If family can't clearly be determined - from *host* and *dualstack_ipv6* is false then :data:`AF_INET` family will - be preferred over :data:`AF_INET6`. - - *type* should be either :data:`SOCK_STREAM` or :data:`SOCK_DGRAM`. - - *backlog* is the queue size passed to :meth:`socket.listen` if - :data:`SOCK_STREAM` *type* is used. If left ``None`` a default reasonable - value is chosen. + Convenience function which creates a :data:`SOCK_STREAM` type socket + bound to *address* (a 2-tuple ``(host, port)``) and return the socket + object. + *family* should be either :data:`AF_INET` or :data:`AF_INET6`. + *backlog* is the queue size passed to :meth:`socket.listen`. *reuse_port* dictates whether to set :data:`SO_REUSEPORT` socket option. - *flags* is a bitmask for :meth:`getaddrinfo()`; if ``None`` - :data:`AI_PASSIVE` is used. - - If *dualstack_ipv6* is tre and the platform supports it the socket will + If *dualstack_ipv6* is true and the platform supports it the socket will be able to accept both IPv4 and IPv6 connections. - In this case the address returned by :meth:`socket.getpeername` when a new - IPv4 connection is accepted will be an IPv6 address represented as an - IPv4-mapped IPv6 address like ``":ffff:127.0.0.1"``. - If *dualstack_ipv6* is false it will explicitly disable this option on - platforms that enable it by default (e.g. Linux). + In this case the address returned by :meth:`socket.getpeername` when an IPv4 + connection occurs will be an IPv6 address represented as an IPv4-mapped IPv6 + address like ``":ffff:127.0.0.1"``. + If *dualstack_ipv6* is false it will explicitly disable this functionality + on platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this `MultipleSocketsListener recipe `__. This parameter can be used in conjunction with :func:`has_dualstack_ipv6`. Here's an echo server example listening on all interfaces, port 8888, - and (possibly) hybrid IPv4/IPv6 support: + accepting both IPv4 and IPv6 connections (if supported): :: import socket with socket.create_server(("", 8888), - dualstack_ipv6=socket.has_dualstack_ipv6()) as server: + dualstack_ipv6=socket.has_dualstack_ipv6()) as server: conn, addr = server.accept() with conn: while True: @@ -650,12 +633,18 @@ The following functions all create :ref:`socket objects `. break conn.send(data) + .. note:: + On POSIX :data:`SO_REUSEADDR` socket option is set in order to immediately + reuse previous sockets which were bound on the same *address* and remained + in TIME_WAIT state. + .. versionadded:: 3.8 .. function:: has_dualstack_ipv6() - Return ``True`` if the platform supports creating a single - :data:`SOCK_STREAM` socket which can accept both IPv4 and IPv6 connections. + Return ``True`` if the platform supports creating a :data:`SOCK_STREAM` + socket which can handle both :data:`AF_INET` or :data:`AF_INET6` + (IPv4 / IPv6) connections. .. versionadded:: 3.8 @@ -1844,8 +1833,8 @@ sends traffic to the first one connected successfully. :: The two examples above can be rewritten by using :meth:`socket.create_server` and :meth:`socket.create_connection` convenience functions. -:meth:`socket.create_server` has the extra advantage of creating an agnostic -IPv4/IPv6 server on platforms supporting this functionality. +If the platform supports it, :meth:`socket.create_server` has the extra +advantage of creating an agnostic IPv4/IPv6 server: :: diff --git a/Lib/socket.py b/Lib/socket.py index 7fde96454daa11..c9e19052806a11 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -747,19 +747,17 @@ def has_dualstack_ipv6(): def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False): - """Convenience function which creates a socket bound to *address* - (a 2-tuple (host, port)) and return the socket object. + """Convenience function which creates a SOCK_STREAM type socket + bound to *address* (a 2-tuple (host, port)) and return the socket + object. *family* should be either AF_INET or AF_INET6. - *backlog* is the queue size passed to socket.listen(). - *reuse_port* dictate whether to use the SO_REUSEPORT socket option. - - *dualstack_ipv6*: if True and the platform supports it it will create - a socket able to accept both IPv4 and IPv6 connections. - When False it will explicitly disable this option on platforms that - enable it by default (e.g. Linux). + *dualstack_ipv6*: if true and the platform supports it, it will + create a socket able to accept both IPv4 and IPv6 connections. + When false it will explicitly disable this option on platforms + that enable it by default (e.g. Linux). >>> with create_server((None, 8000)) as server: ... while True: @@ -771,17 +769,19 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, if dualstack_ipv6: if not has_dualstack_ipv6(): raise ValueError("dualstack_ipv6 not supported on this platform") + if family != AF_INET6: + raise ValueError("dualstack_ipv6 requires AF_INET6 family") sock = socket(family, SOCK_STREAM) try: - # Note about Windows: we don't set SO_REUSEADDR because: + # Note about Windows. We don't set SO_REUSEADDR because: # 1) It's unnecessary: bind() will succeed even in case of a # previous closed socket on the same address and still in # TIME_WAIT state. # 2) If set, another socket will be free to bind() on the same # address, effectively preventing this one from accepting - # connections. Also, it sets the process in a state where it'll - # no longer respond to any signals or graceful kills. More at: - # msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx + # connections. Also, it may set the process in a state where + # it'll no longer respond to any signals or graceful kills. + # See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx if os.name not in ('nt', 'cygwin') and \ hasattr(_socket, 'SO_REUSEADDR'): try: @@ -793,16 +793,15 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, if reuse_port: sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) if has_ipv6 and family == AF_INET6: - if not dualstack_ipv6 and \ - hasattr(_socket, "IPV6_V6ONLY") and \ + if dualstack_ipv6: + sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) + elif hasattr(_socket, "IPV6_V6ONLY") and \ hasattr(_socket, "IPPROTO_IPV6"): # Disable IPv4/IPv6 dual stack support (enabled by # default on Linux) which makes a single socket # listen on both address families in order to # eliminate cross-platform differences. sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) - else: - sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) try: sock.bind(address) except error as err: diff --git a/Lib/test/test_asyncio/test_base_events.py b/Lib/test/test_asyncio/test_base_events.py index 1250e48e19e2df..53854758a27d4c 100644 --- a/Lib/test/test_asyncio/test_base_events.py +++ b/Lib/test/test_asyncio/test_base_events.py @@ -1641,7 +1641,8 @@ class Err(OSError): self.assertTrue(m_sock.close.called) def test_create_datagram_endpoint_sock(self): - sock = socket.create_server(('127.0.0.1', 0), type=socket.SOCK_DGRAM) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('127.0.0.1', 0)) fut = self.loop.create_datagram_endpoint( lambda: MyDatagramProto(create_future=True, loop=self.loop), sock=sock) From 3b3df83ded467f3454f0e5ba5edf4110ccac4dd4 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 01:57:59 +0100 Subject: [PATCH 34/43] fix ftplib bug, skip IPV6 tests --- Lib/ftplib.py | 2 +- Lib/test/test_socket.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/ftplib.py b/Lib/ftplib.py index 5910ca0a17c94c..a9b1aee39e4abb 100644 --- a/Lib/ftplib.py +++ b/Lib/ftplib.py @@ -302,7 +302,7 @@ def sendeprt(self, host, port): def makeport(self): '''Create a new socket and send a PORT command for it.''' - sock = socket.create_server((None, 0), family=self.af, backlog=1) + sock = socket.create_server(("", 0), family=self.af, backlog=1) port = sock.getsockname()[1] # Get proper port host = self.sock.getsockname()[0] # Get proper host if self.af == socket.AF_INET: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index c1e95a1fd724f2..32c3c9618dabc6 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6091,12 +6091,14 @@ def test_reuse_port(self): @unittest.skipIf(not hasattr(_socket, 'IPPROTO_IPV6') or not hasattr(_socket, 'IPV6_V6ONLY'), "IPV6_V6ONLY option not supported") + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_ipv6_only_default(self): with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: assert sock.getsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY) @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_hybrid_ipv6_family(self): with socket.create_server(("::1", 0), family=socket.AF_INET6, dualstack_ipv6=True) as sock: @@ -6163,6 +6165,7 @@ def test_tcp6(self): @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_dual_stack_tcp4(self): with socket.create_server(("", 0), family=socket.AF_INET6, dualstack_ipv6=True) as sock: @@ -6170,6 +6173,7 @@ def test_dual_stack_tcp4(self): @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") + @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_dual_stack_tcp6(self): with socket.create_server(("", 0), family=socket.AF_INET6, dualstack_ipv6=True) as sock: From d75a6002ec15f6e74dd6972abecab8f6bec16dc7 Mon Sep 17 00:00:00 2001 From: Terry Jan Reedy Date: Fri, 15 Feb 2019 01:09:50 -0500 Subject: [PATCH 35/43] idlelib/rpc.py: add missing address argument Omission caused IDLE to crash when started. --- Lib/idlelib/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index 34a403905017d3..e06e9812f7c2a9 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -531,7 +531,7 @@ class RPCClient(SocketIO): def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM): self.listening_sock = socket.create_server( - family=family, type=type, backlog=1) + address, family=family, type=type, backlog=1) def accept(self): working_sock, address = self.listening_sock.accept() From 265b225ea7e2d7ab3e457716d264676b60ca0d85 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Fri, 15 Feb 2019 13:22:04 +0100 Subject: [PATCH 36/43] update doc --- Doc/library/socket.rst | 35 +++++++++++++++-------------------- Lib/socket.py | 14 +++++++------- Lib/test/test_socket.py | 2 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 28ab8f630d9549..8388b9b8b53aca 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -609,34 +609,26 @@ The following functions all create :ref:`socket objects `. be able to accept both IPv4 and IPv6 connections. In this case the address returned by :meth:`socket.getpeername` when an IPv4 connection occurs will be an IPv6 address represented as an IPv4-mapped IPv6 - address like ``":ffff:127.0.0.1"``. + address. If *dualstack_ipv6* is false it will explicitly disable this functionality on platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this `MultipleSocketsListener recipe `__. - This parameter can be used in conjunction with :func:`has_dualstack_ipv6`. - - Here's an echo server example listening on all interfaces, port 8888, - accepting both IPv4 and IPv6 connections (if supported): + This parameter can be used in conjunction with :func:`has_dualstack_ipv6`: :: import socket - with socket.create_server(("", 8888), - dualstack_ipv6=socket.has_dualstack_ipv6()) as server: - conn, addr = server.accept() - with conn: - while True: - data = conn.recv(1024) - if not data: - break - conn.send(data) + if socket.has_dualstack_ipv6(): + s = socket.create_server(addr, family=socket.AF_INET6, dualstack_ipv6=True) + else: + s = socket.create_server(addr) .. note:: - On POSIX :data:`SO_REUSEADDR` socket option is set in order to immediately - reuse previous sockets which were bound on the same *address* and remained - in TIME_WAIT state. + On POSIX platforms :data:`SO_REUSEADDR` socket option is set in order to + immediately reuse previous sockets which were bound on the same *address* + and remained in TIME_WAIT state. .. versionadded:: 3.8 @@ -1833,8 +1825,8 @@ sends traffic to the first one connected successfully. :: The two examples above can be rewritten by using :meth:`socket.create_server` and :meth:`socket.create_connection` convenience functions. -If the platform supports it, :meth:`socket.create_server` has the extra -advantage of creating an agnostic IPv4/IPv6 server: +If supported by the platform, :meth:`socket.create_server` has the extra +advantage of accepting both IPv4 or IPv6 connections on the same socket. :: @@ -1843,7 +1835,10 @@ advantage of creating an agnostic IPv4/IPv6 server: HOST = None PORT = 50007 - s = socket.create_server((HOST, PORT), dualstack_ipv6=socket.has_dualstack_ipv6()) + if socket.has_dualstack_ipv6(): + s = socket.create_server((HOST, PORT), family=socket.AF_INET6, dualstack_ipv6=True) + else: + s = socket.create_server(addr) conn, addr = s.accept() with conn: print('Connected by', addr) diff --git a/Lib/socket.py b/Lib/socket.py index c9e19052806a11..50ea26b8f306f4 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -753,11 +753,11 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, *family* should be either AF_INET or AF_INET6. *backlog* is the queue size passed to socket.listen(). - *reuse_port* dictate whether to use the SO_REUSEPORT socket option. + *reuse_port* dictates whether to use the SO_REUSEPORT socket option. *dualstack_ipv6*: if true and the platform supports it, it will - create a socket able to accept both IPv4 and IPv6 connections. - When false it will explicitly disable this option on platforms - that enable it by default (e.g. Linux). + create an AF_INET6 socket able to accept both IPv4 or IPv6 + connections. When false it will explicitly disable this option on + platforms that enable it by default (e.g. Linux). >>> with create_server((None, 8000)) as server: ... while True: @@ -777,7 +777,7 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, # 1) It's unnecessary: bind() will succeed even in case of a # previous closed socket on the same address and still in # TIME_WAIT state. - # 2) If set, another socket will be free to bind() on the same + # 2) If set, another socket may be free to bind() on the same # address, effectively preventing this one from accepting # connections. Also, it may set the process in a state where # it'll no longer respond to any signals or graceful kills. @@ -805,9 +805,9 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, try: sock.bind(address) except error as err: - err_msg = '%s (while attempting to bind on address %r)' % \ + msg = '%s (while attempting to bind on address %r)' % \ (err.strerror, address) - raise error(err.errno, err_msg) from None + raise error(err.errno, msg) from None sock.listen(backlog or 0) return sock except Exception: diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 32c3c9618dabc6..56b7e44ad83c7a 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6099,7 +6099,7 @@ def test_ipv6_only_default(self): @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') - def test_hybrid_ipv6_family(self): + def test_dualstack_ipv6_family(self): with socket.create_server(("::1", 0), family=socket.AF_INET6, dualstack_ipv6=True) as sock: self.assertEqual(sock.family, socket.AF_INET6) From 426907dc2e24a3b222474ae03ca7cfbe9a02581f Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Sun, 17 Feb 2019 15:51:47 +0100 Subject: [PATCH 37/43] fix doc example + remove UDP-related test code which is no longer used --- Doc/library/socket.rst | 5 +++-- Lib/socket.py | 4 ---- Lib/test/support/__init__.py | 1 + Lib/test/test_socket.py | 24 +++++++----------------- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 8388b9b8b53aca..48f5ed91c85529 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -620,6 +620,7 @@ The following functions all create :ref:`socket objects `. import socket + addr = ("", 8080) # all interfaces, port 8080 if socket.has_dualstack_ipv6(): s = socket.create_server(addr, family=socket.AF_INET6, dualstack_ipv6=True) else: @@ -1833,12 +1834,12 @@ advantage of accepting both IPv4 or IPv6 connections on the same socket. # Echo server program import socket - HOST = None + HOST = "" PORT = 50007 if socket.has_dualstack_ipv6(): s = socket.create_server((HOST, PORT), family=socket.AF_INET6, dualstack_ipv6=True) else: - s = socket.create_server(addr) + s = socket.create_server((HOST, PORT)) conn, addr = s.accept() with conn: print('Connected by', addr) diff --git a/Lib/socket.py b/Lib/socket.py index 50ea26b8f306f4..b8d4045af0a635 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -797,10 +797,6 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) elif hasattr(_socket, "IPV6_V6ONLY") and \ hasattr(_socket, "IPPROTO_IPV6"): - # Disable IPv4/IPv6 dual stack support (enabled by - # default on Linux) which makes a single socket - # listen on both address families in order to - # eliminate cross-platform differences. sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) try: sock.bind(address) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 697182ea775f04..9bf040a173f005 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2944,3 +2944,4 @@ def __fspath__(self): def maybe_get_event_loop_policy(): """Return the global event loop policy if one is set, else return None.""" return asyncio.events._event_loop_policy + diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 56b7e44ad83c7a..d43528263407c7 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6116,23 +6116,19 @@ def tearDown(self): self.thread.join(self.timeout) def echo_server(self, sock): - def run(): - if sock.type == socket.SOCK_STREAM: + def run(sock): + with sock: conn, _ = sock.accept() - conn.settimeout(self.timeout) - event.wait(self.timeout) with conn: + event.wait(self.timeout) msg = conn.recv(1024) if not msg: return conn.sendall(msg) - else: - msg, addr = sock.recvfrom(1024) - sock.sendto(msg, addr) event = threading.Event() sock.settimeout(self.timeout) - self.thread = threading.Thread(target=run) + self.thread = threading.Thread(target=run, args=(sock, )) self.thread.start() event.set() @@ -6143,15 +6139,9 @@ def echo_client(self, sock, connect_host=None): connect_host = sock.getsockname()[0] port = sock.getsockname()[1] server_addr = (connect_host, port) - if sock.type == socket.SOCK_STREAM: - with socket.create_connection(server_addr) as client: - client.sendall(b'foo') - self.assertEqual(client.recv(1024), b'foo') - else: - with socket.socket(sock.family, sock.type) as client: - client.sendto(b'foo', server_addr) - msg, _ = client.recvfrom(1024) - self.assertEqual(msg, b'foo') + with socket.create_connection(server_addr) as client: + client.sendall(b'foo') + self.assertEqual(client.recv(1024), b'foo') def test_tcp4(self): with socket.create_server(("localhost", 0), From 7b75345d6f675b91b83407e5beb86eabc652fe74 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Feb 2019 12:53:17 +0100 Subject: [PATCH 38/43] set backlog=0 instead of None; address doc-related review comments --- Doc/library/socket.rst | 54 ++++++------------------------------ Doc/whatsnew/3.8.rst | 2 +- Lib/socket.py | 4 +-- Lib/test/support/__init__.py | 1 + 4 files changed, 13 insertions(+), 48 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 48f5ed91c85529..17d608f1c3ab58 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -595,25 +595,27 @@ The following functions all create :ref:`socket objects `. .. versionchanged:: 3.2 *source_address* was added. -.. function:: create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False) +.. function:: create_server(address, *, family=AF_INET, backlog=0, reuse_port=False, dualstack_ipv6=False) Convenience function which creates a :data:`SOCK_STREAM` type socket bound to *address* (a 2-tuple ``(host, port)``) and return the socket object. *family* should be either :data:`AF_INET` or :data:`AF_INET6`. - *backlog* is the queue size passed to :meth:`socket.listen`. + *backlog* is the queue size passed to :meth:`socket.listen`; when ``0`` + a default reasonable value is chosen. *reuse_port* dictates whether to set :data:`SO_REUSEPORT` socket option. If *dualstack_ipv6* is true and the platform supports it the socket will - be able to accept both IPv4 and IPv6 connections. - In this case the address returned by :meth:`socket.getpeername` when an IPv4 - connection occurs will be an IPv6 address represented as an IPv4-mapped IPv6 - address. + be able to accept both IPv4 and IPv6 connections, else it will raise + :exc:`ValueError`. Most POSIX platforms are supposed to support this option. + When this option is enabled the address returned by :meth:`socket.getpeername` + when an IPv4 connection occurs will be an IPv6 address represented as an + IPv4-mapped IPv6 address. If *dualstack_ipv6* is false it will explicitly disable this functionality on platforms that enable it by default (e.g. Linux). For platforms not supporting this functionality natively you could use this - `MultipleSocketsListener recipe `__. + `MultipleSocketsListener recipe `__. This parameter can be used in conjunction with :func:`has_dualstack_ipv6`: :: @@ -1823,44 +1825,6 @@ sends traffic to the first one connected successfully. :: data = s.recv(1024) print('Received', repr(data)) - -The two examples above can be rewritten by using :meth:`socket.create_server` -and :meth:`socket.create_connection` convenience functions. -If supported by the platform, :meth:`socket.create_server` has the extra -advantage of accepting both IPv4 or IPv6 connections on the same socket. - -:: - - # Echo server program - import socket - - HOST = "" - PORT = 50007 - if socket.has_dualstack_ipv6(): - s = socket.create_server((HOST, PORT), family=socket.AF_INET6, dualstack_ipv6=True) - else: - s = socket.create_server((HOST, PORT)) - conn, addr = s.accept() - with conn: - print('Connected by', addr) - while True: - data = conn.recv(1024) - if not data: break - conn.send(data) - -:: - - # Echo client program - import socket - - HOST = 'daring.cwi.nl' - PORT = 50007 - with socket.create_connection((HOST, PORT)) as s: - s.sendall(b'Hello, world') - data = s.recv(1024) - print('Received', repr(data)) - - The next example shows how to write a very simple network sniffer with raw sockets on Windows. The example requires administrator privileges to modify the interface:: diff --git a/Doc/whatsnew/3.8.rst b/Doc/whatsnew/3.8.rst index b1d6016c08963c..24151047b4a282 100644 --- a/Doc/whatsnew/3.8.rst +++ b/Doc/whatsnew/3.8.rst @@ -243,7 +243,7 @@ socket ------ Added :meth:`~socket.create_server()` and :meth:`~socket.has_dualstack_ipv6()` -convenience functions to automatize the necessary tasks usually involved when +convenience functions to automate the necessary tasks usually involved when creating a server socket, including accepting both IPv4 and IPv6 connections on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) diff --git a/Lib/socket.py b/Lib/socket.py index b8d4045af0a635..25b637fcef52bf 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -745,7 +745,7 @@ def has_dualstack_ipv6(): return False -def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, +def create_server(address, *, family=AF_INET, backlog=0, reuse_port=False, dualstack_ipv6=False): """Convenience function which creates a SOCK_STREAM type socket bound to *address* (a 2-tuple (host, port)) and return the socket @@ -804,7 +804,7 @@ def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, msg = '%s (while attempting to bind on address %r)' % \ (err.strerror, address) raise error(err.errno, msg) from None - sock.listen(backlog or 0) + sock.listen(backlog) return sock except Exception: sock.close() diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 8c5632477f53fd..b09223149005bb 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2986,3 +2986,4 @@ def collision_stats(nbins, nballs): collisions = k - occupied var = dn*(dn-1)*((dn-2)/dn)**k + meanempty * (1 - meanempty) return float(collisions), float(var.sqrt()) + From 0cf3bb1f8f8a99211077beda2c16dc774cb911d3 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Feb 2019 13:32:12 +0100 Subject: [PATCH 39/43] change unit-tests so that client does not rely on getaddrinfo() - better be explicit what family the client is supposed to use --- Lib/test/test_socket.py | 42 ++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index d43528263407c7..618d7fbd6aa379 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6132,42 +6132,46 @@ def run(sock): self.thread.start() event.set() - def echo_client(self, sock, connect_host=None): - self.echo_server(sock) - server_addr = sock.getsockname()[:2] - if connect_host is None: - connect_host = sock.getsockname()[0] - port = sock.getsockname()[1] - server_addr = (connect_host, port) - with socket.create_connection(server_addr) as client: - client.sendall(b'foo') - self.assertEqual(client.recv(1024), b'foo') + def echo_client(self, addr, family): + with socket.socket(family=family) as sock: + sock.settimeout(self.timeout) + sock.connect(addr) + sock.sendall(b'foo') + self.assertEqual(sock.recv(1024), b'foo') def test_tcp4(self): - with socket.create_server(("localhost", 0), - family=socket.AF_INET) as sock: - self.echo_client(sock) + port = support.find_unused_port() + with socket.create_server(("localhost", port)) as sock: + self.echo_server(sock) + self.echo_client(("127.0.0.1", port), socket.AF_INET) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): - with socket.create_server(("::1", 0), family=socket.AF_INET6) as sock: - self.echo_client(sock) + port = support.find_unused_port() + with socket.create_server(("::1", port), + family=socket.AF_INET6) as sock: + self.echo_server(sock) + self.echo_client(("::1", port), socket.AF_INET6) @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_dual_stack_tcp4(self): - with socket.create_server(("", 0), family=socket.AF_INET6, + port = support.find_unused_port() + with socket.create_server(("::", port), family=socket.AF_INET6, dualstack_ipv6=True) as sock: - self.echo_client(sock, connect_host="127.0.0.1") + self.echo_server(sock) + self.echo_client(("127.0.0.1", port), socket.AF_INET) @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_dual_stack_tcp6(self): - with socket.create_server(("", 0), family=socket.AF_INET6, + port = support.find_unused_port() + with socket.create_server(("::", port), family=socket.AF_INET6, dualstack_ipv6=True) as sock: - self.echo_client(sock, connect_host="::1") + self.echo_server(sock) + self.echo_client(("127.0.0.1", port), socket.AF_INET) def test_main(): From 12bbf0cf922372022e17484ea2e6bd9bc3dcdf87 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Mon, 18 Feb 2019 13:50:03 +0100 Subject: [PATCH 40/43] fix wrong client addr + fix travis test due to extra whitespace --- Lib/test/support/__init__.py | 1 - Lib/test/test_socket.py | 16 +++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index b09223149005bb..8c5632477f53fd 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2986,4 +2986,3 @@ def collision_stats(nbins, nballs): collisions = k - occupied var = dn*(dn-1)*((dn-2)/dn)**k + meanempty * (1 - meanempty) return float(collisions), float(var.sqrt()) - diff --git a/Lib/test/test_socket.py b/Lib/test/test_socket.py index 618d7fbd6aa379..2f824538ec1757 100644 --- a/Lib/test/test_socket.py +++ b/Lib/test/test_socket.py @@ -6141,24 +6141,26 @@ def echo_client(self, addr, family): def test_tcp4(self): port = support.find_unused_port() - with socket.create_server(("localhost", port)) as sock: + with socket.create_server(("", port)) as sock: self.echo_server(sock) self.echo_client(("127.0.0.1", port), socket.AF_INET) @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') def test_tcp6(self): port = support.find_unused_port() - with socket.create_server(("::1", port), + with socket.create_server(("", port), family=socket.AF_INET6) as sock: self.echo_server(sock) self.echo_client(("::1", port), socket.AF_INET6) + # --- dual stack tests + @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') - def test_dual_stack_tcp4(self): + def test_dual_stack_client_v4(self): port = support.find_unused_port() - with socket.create_server(("::", port), family=socket.AF_INET6, + with socket.create_server(("", port), family=socket.AF_INET6, dualstack_ipv6=True) as sock: self.echo_server(sock) self.echo_client(("127.0.0.1", port), socket.AF_INET) @@ -6166,12 +6168,12 @@ def test_dual_stack_tcp4(self): @unittest.skipIf(not socket.has_dualstack_ipv6(), "dualstack_ipv6 not supported") @unittest.skipUnless(support.IPV6_ENABLED, 'IPv6 required for this test') - def test_dual_stack_tcp6(self): + def test_dual_stack_client_v6(self): port = support.find_unused_port() - with socket.create_server(("::", port), family=socket.AF_INET6, + with socket.create_server(("", port), family=socket.AF_INET6, dualstack_ipv6=True) as sock: self.echo_server(sock) - self.echo_client(("127.0.0.1", port), socket.AF_INET) + self.echo_client(("::1", port), socket.AF_INET6) def test_main(): From caa7605b0166831ce8812dc556d594bd93d37ad4 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" Date: Mon, 18 Feb 2019 23:12:39 -0800 Subject: [PATCH 41/43] automatize -> automate --- .../next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst index b10dff03d17434..0601ac915fc84c 100644 --- a/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst +++ b/Misc/NEWS.d/next/Library/2019-02-07-20-25-39.bpo-35934.QmfNmY.rst @@ -1,4 +1,4 @@ Added :meth:`~socket.create_server()` and :meth:`~socket.has_dualstack_ipv6()` -convenience functions to automatize the necessary tasks usually involved when +convenience functions to automate the necessary tasks usually involved when creating a server socket, including accepting both IPv4 and IPv6 connections on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) From f7868847da9f84cb68605b4b94d8fcc205e0766e Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Wed, 6 Mar 2019 16:07:29 +0100 Subject: [PATCH 42/43] update doc; catch socket.error instead of Exception; remove idlelib integration --- Doc/library/socket.rst | 9 ++++----- Lib/idlelib/rpc.py | 5 +++-- Lib/socket.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 17d608f1c3ab58..800058b1af373c 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -604,7 +604,7 @@ The following functions all create :ref:`socket objects `. *family* should be either :data:`AF_INET` or :data:`AF_INET6`. *backlog* is the queue size passed to :meth:`socket.listen`; when ``0`` a default reasonable value is chosen. - *reuse_port* dictates whether to set :data:`SO_REUSEPORT` socket option. + *reuse_port* dictates whether to set the :data:`SO_REUSEPORT` socket option. If *dualstack_ipv6* is true and the platform supports it the socket will be able to accept both IPv4 and IPv6 connections, else it will raise @@ -629,7 +629,7 @@ The following functions all create :ref:`socket objects `. s = socket.create_server(addr) .. note:: - On POSIX platforms :data:`SO_REUSEADDR` socket option is set in order to + On POSIX platforms the :data:`SO_REUSEADDR` socket option is set in order to immediately reuse previous sockets which were bound on the same *address* and remained in TIME_WAIT state. @@ -637,9 +637,8 @@ The following functions all create :ref:`socket objects `. .. function:: has_dualstack_ipv6() - Return ``True`` if the platform supports creating a :data:`SOCK_STREAM` - socket which can handle both :data:`AF_INET` or :data:`AF_INET6` - (IPv4 / IPv6) connections. + Return ``True`` if the platform supports creating a TCP socket which can + handle both IPv4 and IPv6 connections. .. versionadded:: 3.8 diff --git a/Lib/idlelib/rpc.py b/Lib/idlelib/rpc.py index e06e9812f7c2a9..9962477cc56185 100644 --- a/Lib/idlelib/rpc.py +++ b/Lib/idlelib/rpc.py @@ -530,8 +530,9 @@ class RPCClient(SocketIO): nextseq = 1 # Requests coming from the client are odd numbered def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM): - self.listening_sock = socket.create_server( - address, family=family, type=type, backlog=1) + self.listening_sock = socket.socket(family, type) + self.listening_sock.bind(address) + self.listening_sock.listen(1) def accept(self): working_sock, address = self.listening_sock.accept() diff --git a/Lib/socket.py b/Lib/socket.py index 25b637fcef52bf..454691cc11d740 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -806,7 +806,7 @@ def create_server(address, *, family=AF_INET, backlog=0, reuse_port=False, raise error(err.errno, msg) from None sock.listen(backlog) return sock - except Exception: + except error: sock.close() raise From 5b491253e6952dfb1115ef43e13d1076cb5a5549 Mon Sep 17 00:00:00 2001 From: Giampaolo Rodola Date: Thu, 28 Mar 2019 15:46:16 +0100 Subject: [PATCH 43/43] update doc (remove ref to activestate recipe) --- Doc/library/socket.rst | 16 +++++++--------- Lib/socket.py | 2 +- Lib/test/support/__init__.py | 1 - 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 800058b1af373c..b4a07bd5d5d211 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -597,9 +597,8 @@ The following functions all create :ref:`socket objects `. .. function:: create_server(address, *, family=AF_INET, backlog=0, reuse_port=False, dualstack_ipv6=False) - Convenience function which creates a :data:`SOCK_STREAM` type socket - bound to *address* (a 2-tuple ``(host, port)``) and return the socket - object. + Convenience function which creates a TCP socket bound to *address* (a 2-tuple + ``(host, port)``) and return the socket object. *family* should be either :data:`AF_INET` or :data:`AF_INET6`. *backlog* is the queue size passed to :meth:`socket.listen`; when ``0`` @@ -608,14 +607,13 @@ The following functions all create :ref:`socket objects `. If *dualstack_ipv6* is true and the platform supports it the socket will be able to accept both IPv4 and IPv6 connections, else it will raise - :exc:`ValueError`. Most POSIX platforms are supposed to support this option. - When this option is enabled the address returned by :meth:`socket.getpeername` - when an IPv4 connection occurs will be an IPv6 address represented as an - IPv4-mapped IPv6 address. + :exc:`ValueError`. Most POSIX platforms and Windows are supposed to support + this functionality. + When this functionality is enabled the address returned by + :meth:`socket.getpeername` when an IPv4 connection occurs will be an IPv6 + address represented as an IPv4-mapped IPv6 address. If *dualstack_ipv6* is false it will explicitly disable this functionality on platforms that enable it by default (e.g. Linux). - For platforms not supporting this functionality natively you could use this - `MultipleSocketsListener recipe `__. This parameter can be used in conjunction with :func:`has_dualstack_ipv6`: :: diff --git a/Lib/socket.py b/Lib/socket.py index 454691cc11d740..2e51cd16f3ac18 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -777,7 +777,7 @@ def create_server(address, *, family=AF_INET, backlog=0, reuse_port=False, # 1) It's unnecessary: bind() will succeed even in case of a # previous closed socket on the same address and still in # TIME_WAIT state. - # 2) If set, another socket may be free to bind() on the same + # 2) If set, another socket is free to bind() on the same # address, effectively preventing this one from accepting # connections. Also, it may set the process in a state where # it'll no longer respond to any signals or graceful kills. diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 792ad3e8fe4c39..5bd15a2feae9d7 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -2978,7 +2978,6 @@ def maybe_get_event_loop_policy(): """Return the global event loop policy if one is set, else return None.""" return asyncio.events._event_loop_policy - # Helpers for testing hashing. NHASHBITS = sys.hash_info.width # number of bits in hash() result assert NHASHBITS in (32, 64)