Skip to content

Commit f7686c1

Browse files
Neil Aspinallasvetlov
authored andcommitted
bpo-29970: Add timeout for SSL handshake in asyncio
10 seconds by default.
1 parent 4b96593 commit f7686c1

12 files changed

Lines changed: 209 additions & 85 deletions

File tree

Doc/library/asyncio-eventloop.rst

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Tasks
261261
Creating connections
262262
--------------------
263263

264-
.. coroutinemethod:: AbstractEventLoop.create_connection(protocol_factory, host=None, port=None, \*, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None)
264+
.. coroutinemethod:: AbstractEventLoop.create_connection(protocol_factory, host=None, port=None, \*, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=10.0)
265265

266266
Create a streaming transport connection to a given Internet *host* and
267267
*port*: socket family :py:data:`~socket.AF_INET` or
@@ -325,6 +325,13 @@ Creating connections
325325
to bind the socket to locally. The *local_host* and *local_port*
326326
are looked up using getaddrinfo(), similarly to *host* and *port*.
327327

328+
* *ssl_handshake_timeout* is (for an SSL connection) the time in seconds
329+
to wait for the SSL handshake to complete before aborting the connection.
330+
331+
.. versionadded:: 3.7
332+
333+
The *ssl_handshake_timeout* parameter.
334+
328335
.. versionchanged:: 3.5
329336

330337
On Windows with :class:`ProactorEventLoop`, SSL/TLS is now supported.
@@ -386,7 +393,7 @@ Creating connections
386393
:ref:`UDP echo server protocol <asyncio-udp-echo-server-protocol>` examples.
387394

388395

389-
.. coroutinemethod:: AbstractEventLoop.create_unix_connection(protocol_factory, path=None, \*, ssl=None, sock=None, server_hostname=None)
396+
.. coroutinemethod:: AbstractEventLoop.create_unix_connection(protocol_factory, path=None, \*, ssl=None, sock=None, server_hostname=None, ssl_handshake_timeout=10.0)
390397

391398
Create UNIX connection: socket family :py:data:`~socket.AF_UNIX`, socket
392399
type :py:data:`~socket.SOCK_STREAM`. The :py:data:`~socket.AF_UNIX` socket
@@ -404,6 +411,10 @@ Creating connections
404411

405412
Availability: UNIX.
406413

414+
.. versionadded:: 3.7
415+
416+
The *ssl_handshake_timeout* parameter.
417+
407418
.. versionchanged:: 3.7
408419

409420
The *path* parameter can now be a :class:`~pathlib.Path` object.
@@ -412,7 +423,7 @@ Creating connections
412423
Creating listening connections
413424
------------------------------
414425

415-
.. coroutinemethod:: AbstractEventLoop.create_server(protocol_factory, host=None, port=None, \*, family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE, sock=None, backlog=100, ssl=None, reuse_address=None, reuse_port=None)
426+
.. coroutinemethod:: AbstractEventLoop.create_server(protocol_factory, host=None, port=None, \*, family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE, sock=None, backlog=100, ssl=None, reuse_address=None, reuse_port=None, ssl_handshake_timeout=10.0)
416427

417428
Create a TCP server (socket type :data:`~socket.SOCK_STREAM`) bound to
418429
*host* and *port*.
@@ -456,6 +467,13 @@ Creating listening connections
456467
set this flag when being created. This option is not supported on
457468
Windows.
458469

470+
* *ssl_handshake_timeout* is (for an SSL server) the time in seconds to wait
471+
for the SSL handshake to complete before aborting the connection.
472+
473+
.. versionadded:: 3.7
474+
475+
The *ssl_handshake_timeout* parameter.
476+
459477
.. versionchanged:: 3.5
460478

461479
On Windows with :class:`ProactorEventLoop`, SSL/TLS is now supported.
@@ -470,7 +488,7 @@ Creating listening connections
470488
The *host* parameter can now be a sequence of strings.
471489

472490

473-
.. coroutinemethod:: AbstractEventLoop.create_unix_server(protocol_factory, path=None, \*, sock=None, backlog=100, ssl=None)
491+
.. coroutinemethod:: AbstractEventLoop.create_unix_server(protocol_factory, path=None, \*, sock=None, backlog=100, ssl=None, ssl_handshake_timeout=10.0)
474492

475493
Similar to :meth:`AbstractEventLoop.create_server`, but specific to the
476494
socket family :py:data:`~socket.AF_UNIX`.
@@ -481,11 +499,15 @@ Creating listening connections
481499

482500
Availability: UNIX.
483501

502+
.. versionadded:: 3.7
503+
504+
The *ssl_handshake_timeout* parameter.
505+
484506
.. versionchanged:: 3.7
485507

486508
The *path* parameter can now be a :class:`~pathlib.Path` object.
487509

488-
.. coroutinemethod:: BaseEventLoop.connect_accepted_socket(protocol_factory, sock, \*, ssl=None)
510+
.. coroutinemethod:: BaseEventLoop.connect_accepted_socket(protocol_factory, sock, \*, ssl=None, ssl_handshake_timeout=10.0)
489511

490512
Handle an accepted connection.
491513

@@ -500,8 +522,15 @@ Creating listening connections
500522
* *ssl* can be set to an :class:`~ssl.SSLContext` to enable SSL over the
501523
accepted connections.
502524

525+
* *ssl_handshake_timeout* is (for an SSL connection) the time in seconds to
526+
wait for the SSL handshake to complete before aborting the connection.
527+
503528
When completed it returns a ``(transport, protocol)`` pair.
504529

530+
.. versionadded:: 3.7
531+
532+
The *ssl_handshake_timeout* parameter.
533+
505534
.. versionadded:: 3.5.3
506535

507536

Lib/asyncio/base_events.py

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import warnings
3030
import weakref
3131

32+
from . import constants
3233
from . import coroutines
3334
from . import events
3435
from . import futures
@@ -275,9 +276,11 @@ def _make_socket_transport(self, sock, protocol, waiter=None, *,
275276
"""Create socket transport."""
276277
raise NotImplementedError
277278

278-
def _make_ssl_transport(self, rawsock, protocol, sslcontext, waiter=None,
279-
*, server_side=False, server_hostname=None,
280-
extra=None, server=None):
279+
def _make_ssl_transport(
280+
self, rawsock, protocol, sslcontext, waiter=None,
281+
*, server_side=False, server_hostname=None,
282+
extra=None, server=None,
283+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
281284
"""Create SSL transport."""
282285
raise NotImplementedError
283286

@@ -635,10 +638,12 @@ async def getnameinfo(self, sockaddr, flags=0):
635638
return await self.run_in_executor(
636639
None, socket.getnameinfo, sockaddr, flags)
637640

638-
async def create_connection(self, protocol_factory, host=None, port=None,
639-
*, ssl=None, family=0,
640-
proto=0, flags=0, sock=None,
641-
local_addr=None, server_hostname=None):
641+
async def create_connection(
642+
self, protocol_factory, host=None, port=None,
643+
*, ssl=None, family=0,
644+
proto=0, flags=0, sock=None,
645+
local_addr=None, server_hostname=None,
646+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
642647
"""Connect to a TCP server.
643648
644649
Create a streaming transport connection to a given Internet host and
@@ -751,7 +756,8 @@ async def create_connection(self, protocol_factory, host=None, port=None,
751756
f'A Stream Socket was expected, got {sock!r}')
752757

753758
transport, protocol = await self._create_connection_transport(
754-
sock, protocol_factory, ssl, server_hostname)
759+
sock, protocol_factory, ssl, server_hostname,
760+
ssl_handshake_timeout=ssl_handshake_timeout)
755761
if self._debug:
756762
# Get the socket from the transport because SSL transport closes
757763
# the old socket and creates a new SSL socket
@@ -760,8 +766,10 @@ async def create_connection(self, protocol_factory, host=None, port=None,
760766
sock, host, port, transport, protocol)
761767
return transport, protocol
762768

763-
async def _create_connection_transport(self, sock, protocol_factory, ssl,
764-
server_hostname, server_side=False):
769+
async def _create_connection_transport(
770+
self, sock, protocol_factory, ssl,
771+
server_hostname, server_side=False,
772+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
765773

766774
sock.setblocking(False)
767775

@@ -771,7 +779,8 @@ async def _create_connection_transport(self, sock, protocol_factory, ssl,
771779
sslcontext = None if isinstance(ssl, bool) else ssl
772780
transport = self._make_ssl_transport(
773781
sock, protocol, sslcontext, waiter,
774-
server_side=server_side, server_hostname=server_hostname)
782+
server_side=server_side, server_hostname=server_hostname,
783+
ssl_handshake_timeout=ssl_handshake_timeout)
775784
else:
776785
transport = self._make_socket_transport(sock, protocol, waiter)
777786

@@ -929,15 +938,17 @@ async def _create_server_getaddrinfo(self, host, port, family, flags):
929938
raise OSError(f'getaddrinfo({host!r}) returned empty list')
930939
return infos
931940

932-
async def create_server(self, protocol_factory, host=None, port=None,
933-
*,
934-
family=socket.AF_UNSPEC,
935-
flags=socket.AI_PASSIVE,
936-
sock=None,
937-
backlog=100,
938-
ssl=None,
939-
reuse_address=None,
940-
reuse_port=None):
941+
async def create_server(
942+
self, protocol_factory, host=None, port=None,
943+
*,
944+
family=socket.AF_UNSPEC,
945+
flags=socket.AI_PASSIVE,
946+
sock=None,
947+
backlog=100,
948+
ssl=None,
949+
reuse_address=None,
950+
reuse_port=None,
951+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
941952
"""Create a TCP server.
942953
943954
The host parameter can be a string, in that case the TCP server is
@@ -1026,13 +1037,16 @@ async def create_server(self, protocol_factory, host=None, port=None,
10261037
for sock in sockets:
10271038
sock.listen(backlog)
10281039
sock.setblocking(False)
1029-
self._start_serving(protocol_factory, sock, ssl, server, backlog)
1040+
self._start_serving(protocol_factory, sock, ssl, server, backlog,
1041+
ssl_handshake_timeout)
10301042
if self._debug:
10311043
logger.info("%r is serving", server)
10321044
return server
10331045

1034-
async def connect_accepted_socket(self, protocol_factory, sock,
1035-
*, ssl=None):
1046+
async def connect_accepted_socket(
1047+
self, protocol_factory, sock,
1048+
*, ssl=None,
1049+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
10361050
"""Handle an accepted connection.
10371051
10381052
This is used by servers that accept connections outside of
@@ -1045,7 +1059,8 @@ async def connect_accepted_socket(self, protocol_factory, sock,
10451059
raise ValueError(f'A Stream Socket was expected, got {sock!r}')
10461060

10471061
transport, protocol = await self._create_connection_transport(
1048-
sock, protocol_factory, ssl, '', server_side=True)
1062+
sock, protocol_factory, ssl, '', server_side=True,
1063+
ssl_handshake_timeout=ssl_handshake_timeout)
10491064
if self._debug:
10501065
# Get the socket from the transport because SSL transport closes
10511066
# the old socket and creates a new SSL socket

Lib/asyncio/constants.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88
# The larger the number, the slower the operation in debug mode
99
# (see extract_stack() in format_helpers.py).
1010
DEBUG_STACK_DEPTH = 10
11+
12+
# Number of seconds to wait for SSL handshake to complete
13+
SSL_HANDSHAKE_TIMEOUT = 10.0

Lib/asyncio/events.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -250,16 +250,20 @@ async def getaddrinfo(self, host, port, *,
250250
async def getnameinfo(self, sockaddr, flags=0):
251251
raise NotImplementedError
252252

253-
async def create_connection(self, protocol_factory, host=None, port=None,
254-
*, ssl=None, family=0, proto=0,
255-
flags=0, sock=None, local_addr=None,
256-
server_hostname=None):
257-
raise NotImplementedError
258-
259-
async def create_server(self, protocol_factory, host=None, port=None,
260-
*, family=socket.AF_UNSPEC,
261-
flags=socket.AI_PASSIVE, sock=None, backlog=100,
262-
ssl=None, reuse_address=None, reuse_port=None):
253+
async def create_connection(
254+
self, protocol_factory, host=None, port=None,
255+
*, ssl=None, family=0, proto=0,
256+
flags=0, sock=None, local_addr=None,
257+
server_hostname=None,
258+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
259+
raise NotImplementedError
260+
261+
async def create_server(
262+
self, protocol_factory, host=None, port=None,
263+
*, family=socket.AF_UNSPEC,
264+
flags=socket.AI_PASSIVE, sock=None, backlog=100,
265+
ssl=None, reuse_address=None, reuse_port=None,
266+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
263267
"""A coroutine which creates a TCP server bound to host and port.
264268
265269
The return value is a Server object which can be used to stop
@@ -294,16 +298,25 @@ async def create_server(self, protocol_factory, host=None, port=None,
294298
the same port as other existing endpoints are bound to, so long as
295299
they all set this flag when being created. This option is not
296300
supported on Windows.
301+
302+
ssl_handshake_timeout is the time in seconds that an SSL server
303+
will wait for completion of the SSL handshake before aborting the
304+
connection. Default is 10s, longer timeouts may increase vulnerability
305+
to DoS attacks (see https://support.f5.com/csp/article/K13834)
297306
"""
298307
raise NotImplementedError
299308

300-
async def create_unix_connection(self, protocol_factory, path=None, *,
301-
ssl=None, sock=None,
302-
server_hostname=None):
309+
async def create_unix_connection(
310+
self, protocol_factory, path=None, *,
311+
ssl=None, sock=None,
312+
server_hostname=None,
313+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
303314
raise NotImplementedError
304315

305-
async def create_unix_server(self, protocol_factory, path=None, *,
306-
sock=None, backlog=100, ssl=None):
316+
async def create_unix_server(
317+
self, protocol_factory, path=None, *,
318+
sock=None, backlog=100, ssl=None,
319+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
307320
"""A coroutine which creates a UNIX Domain Socket server.
308321
309322
The return value is a Server object, which can be used to stop
@@ -320,6 +333,9 @@ async def create_unix_server(self, protocol_factory, path=None, *,
320333
321334
ssl can be set to an SSLContext to enable SSL over the
322335
accepted connections.
336+
337+
ssl_handshake_timeout is the time in seconds that an SSL server
338+
will wait for the SSL handshake to complete (defaults to 10s).
323339
"""
324340
raise NotImplementedError
325341

Lib/asyncio/proactor_events.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -389,11 +389,15 @@ def _make_socket_transport(self, sock, protocol, waiter=None,
389389
return _ProactorSocketTransport(self, sock, protocol, waiter,
390390
extra, server)
391391

392-
def _make_ssl_transport(self, rawsock, protocol, sslcontext, waiter=None,
393-
*, server_side=False, server_hostname=None,
394-
extra=None, server=None):
395-
ssl_protocol = sslproto.SSLProtocol(self, protocol, sslcontext, waiter,
396-
server_side, server_hostname)
392+
def _make_ssl_transport(
393+
self, rawsock, protocol, sslcontext, waiter=None,
394+
*, server_side=False, server_hostname=None,
395+
extra=None, server=None,
396+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
397+
ssl_protocol = sslproto.SSLProtocol(
398+
self, protocol, sslcontext, waiter,
399+
server_side, server_hostname,
400+
ssl_handshake_timeout=ssl_handshake_timeout)
397401
_ProactorSocketTransport(self, rawsock, ssl_protocol,
398402
extra=extra, server=server)
399403
return ssl_protocol._app_transport
@@ -486,7 +490,8 @@ def _write_to_self(self):
486490
self._csock.send(b'\0')
487491

488492
def _start_serving(self, protocol_factory, sock,
489-
sslcontext=None, server=None, backlog=100):
493+
sslcontext=None, server=None, backlog=100,
494+
ssl_handshake_timeout=constants.SSL_HANDSHAKE_TIMEOUT):
490495

491496
def loop(f=None):
492497
try:
@@ -499,7 +504,8 @@ def loop(f=None):
499504
if sslcontext is not None:
500505
self._make_ssl_transport(
501506
conn, protocol, sslcontext, server_side=True,
502-
extra={'peername': addr}, server=server)
507+
extra={'peername': addr}, server=server,
508+
ssl_handshake_timeout=ssl_handshake_timeout)
503509
else:
504510
self._make_socket_transport(
505511
conn, protocol,

0 commit comments

Comments
 (0)