Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion Lib/asyncio/sslproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def close(self):
self._closed = True
self._ssl_protocol._start_shutdown()
else:
self._ssl_protocol = None
self._ssl_protocol._release_ssl()

def __del__(self, _warnings=warnings):
if not self._closed:
Expand Down Expand Up @@ -326,6 +326,7 @@ def __init__(self, loop, app_protocol, sslcontext, waiter,
self._outgoing = ssl.MemoryBIO()
self._state = SSLProtocolState.UNWRAPPED
self._conn_lost = 0 # Set when connection_lost called
self._ssl_release_pending = False
if call_connection_made:
self._app_state = AppProtocolState.STATE_INIT
else:
Expand Down Expand Up @@ -382,6 +383,20 @@ def _get_app_transport(self):
self._app_transport_created = True
return self._app_transport

def _release_ssl(self):
# Called when the app closes an already closed transport. The app
# transport can outlive the connection, e.g. when the app protocol
# keeps a reference to it, so it should not keep the SSL object and
# context alive. The shutdown still needs them until the connection
# is lost.
if self._transport is not None or not self._conn_lost:
self._ssl_release_pending = True
return
self._sslobj = None
self._sslcontext = None
self._extra.pop('ssl_object', None)
self._extra.pop('sslcontext', None)

def _is_transport_closing(self):
return self._transport is not None and self._transport.is_closing()

Expand Down Expand Up @@ -430,6 +445,8 @@ def connection_lost(self, exc):
if self._handshake_timeout_handle:
self._handshake_timeout_handle.cancel()
self._handshake_timeout_handle = None
if self._ssl_release_pending:
self._release_ssl()

def get_buffer(self, n):
want = n
Expand Down
42 changes: 42 additions & 0 deletions Lib/test/test_asyncio/test_sslproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,48 @@ def test_write_after_closing(self):
# should not raise
self.assertIsNone(transp.write(b'data'))

def test_close_after_connection_lost(self):
# gh-160964: closing the transport after the connection is lost,
# e.g. from the app's connection_lost(), must keep it usable but
# release the SSL object and context.
ssl_proto = self.ssl_protocol()
self.connection_made(ssl_proto)
transp = ssl_proto._app_transport
self.assertIsNotNone(transp.get_extra_info('ssl_object'))
self.assertIsNotNone(transp.get_extra_info('sslcontext'))
ssl_proto.connection_lost(None)
transp.close()

self.assertIsNone(ssl_proto._sslobj)
self.assertIsNone(ssl_proto._sslcontext)
self.assertIsNone(transp.get_extra_info('ssl_object'))
self.assertIsNone(transp.get_extra_info('sslcontext'))
# should not raise
self.assertTrue(transp.is_closing())
self.assertIsNone(transp.write(b'data'))
self.assertEqual(transp.get_write_buffer_size(), 0)
new_app_proto = asyncio.Protocol()
transp.set_protocol(new_app_proto)
self.assertIs(transp.get_protocol(), new_app_proto)
transp.close()

def test_close_twice_during_shutdown(self):
# The shutdown still needs the SSL object: it is released only once
# the connection is lost.
ssl_proto = self.ssl_protocol()
self.connection_made(ssl_proto)
transp = ssl_proto._app_transport
transp.close()
transp.close()
self.assertIsNotNone(ssl_proto._sslobj)
self.assertIsNotNone(transp.get_extra_info('ssl_object'))

ssl_proto.connection_lost(None)
self.assertIsNone(ssl_proto._sslobj)
self.assertIsNone(ssl_proto._sslcontext)
self.assertIsNone(transp.get_extra_info('ssl_object'))
self.assertIsNone(transp.get_extra_info('sslcontext'))

def test_pause_reading_if_ssl_replies_are_not_read(self):
# Output which OpenSSL produces on its own while reading (alerts,
# KeyUpdate, ...) must not pile up if the peer does not read it.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix :exc:`AttributeError` from the methods of an :mod:`asyncio` SSL transport
that was closed after its connection was lost, for example from the
protocol's :meth:`~asyncio.BaseProtocol.connection_lost`. This affected the
fallback of :meth:`loop.sendfile() <asyncio.loop.sendfile>` when the peer
closed the connection during the transfer.
Loading