Skip to content
Merged
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: 17 additions & 2 deletions Lib/asyncio/sslproto.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,6 @@ def close(self):
if not self._closed:
self._closed = True
self._ssl_protocol._start_shutdown()
else:
self._ssl_protocol = None

def __del__(self, _warnings=warnings):
if not self._closed:
Expand Down Expand Up @@ -412,12 +410,14 @@ def connection_lost(self, exc):
# The app protocol gets connection_lost() only if it got
# connection_made(). After start_tls() that is already the case
# while the handshake is in progress.
app_notified = False
if (
self._app_state == AppProtocolState.STATE_CON_MADE or
self._app_state == AppProtocolState.STATE_EOF
):
self._app_state = AppProtocolState.STATE_CON_LOST
self._loop.call_soon(self._app_protocol.connection_lost, exc)
app_notified = True
self._set_state(SSLProtocolState.UNWRAPPED)
self._transport = None
self._app_transport = None
Expand All @@ -430,6 +430,21 @@ def connection_lost(self, exc):
if self._handshake_timeout_handle:
self._handshake_timeout_handle.cancel()
self._handshake_timeout_handle = None
if app_notified:
# After the app protocol's connection_lost() (scheduled above),
# so that it can still read the TLS extra info.
self._loop.call_soon(self._release_tls_state)
else:
self._release_tls_state()

def _release_tls_state(self):
# The connection is gone: drop everything that refers to the
# SSLContext, so that an application holding the transport does
# not keep the context alive.
self._sslobj = None
self._sslcontext = None
self._extra.pop('sslcontext', None)
self._extra.pop('ssl_object', None)

def get_buffer(self, n):
want = n
Expand Down
32 changes: 32 additions & 0 deletions Lib/test/test_asyncio/test_sendfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,38 @@ def test_sendfile_ssl_close_peer_after_receiving(self):
self.assertEqual(srv_proto.data, self.DATA)
self.assertEqual(self.file.tell(), len(self.DATA))

def test_sendfile_ssl_close_peer_in_the_middle_closes_transport(self):
# The protocol closes its transport when the connection is lost, as
# aiohttp's does. That close() must leave a transport on which the
# fallback can still restore the protocol: sendfile() raises
# ConnectionError, not AttributeError.
srv_proto, cli_proto = self.prepare_sendfile(
is_ssl=True, close_after=1024)
connection_lost = cli_proto.connection_lost

def close_on_lost(exc):
cli_proto.transport.close()
connection_lost(exc)

cli_proto.connection_lost = close_on_lost
# Read the file synchronously, so that the fallback only waits in
# drain(). A read in the executor that completes in the same loop
# iteration as the reset lets the fallback finish before close() is
# called, which hides the bug.
def run_in_executor(executor, func, *args):
fut = self.loop.create_future()
fut.set_result(func(*args))
return fut

with mock.patch.object(self.loop, 'run_in_executor', run_in_executor):
with self.assertRaises(ConnectionError):
self.run_loop(
self.loop.sendfile(cli_proto.transport, self.file))
self.run_loop(srv_proto.done)
self.run_loop(cli_proto.done)
self.assertTrue(1024 <= srv_proto.nbytes < len(self.DATA),
srv_proto.nbytes)

# On Solaris, lowering SO_RCVBUF on a TCP connection after it has been
# established has no effect. Due to its age, this bug affects both Oracle
# Solaris as well as all other OpenSolaris forks (unless they fixed it
Expand Down
7 changes: 6 additions & 1 deletion Lib/test/test_asyncio/test_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -788,16 +788,21 @@ async def client(addr):
await on_eof

tr.close()
return tr

with self.tcp_server(serve, timeout=self.TIMEOUT) as srv:
self.loop.run_until_complete(
tr = self.loop.run_until_complete(
asyncio.wait_for(client(srv.addr),
timeout=support.SHORT_TIMEOUT))
# Let the protocol's connection_lost() run.
self.loop.run_until_complete(asyncio.sleep(0))

# No garbage is left for SSL client from loop.create_connection, even
# if user stores the SSLTransport in corresponding protocol instance
# (and keeps the transport after the connection is lost).
client_context = weakref.ref(client_context)
self.assertIsNone(client_context())
del tr

def test_start_tls_client_buf_proto_1(self):
HELLO_MSG = b'1' * self.PAYLOAD_SIZE
Expand Down
20 changes: 20 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,26 @@ def test_write_after_closing(self):
# should not raise
self.assertIsNone(transp.write(b'data'))

def test_transport_usable_after_connection_lost_and_close(self):
# connection_lost() marks the app transport closed, so the
# application's own close() afterwards (often made from its
# protocol's connection_lost()) must not be taken as a second
# close() that leaves the transport without its SSL protocol.
ssl_proto = self.ssl_protocol()
self.connection_made(ssl_proto)
transp = ssl_proto._app_transport
ssl_proto.connection_lost(None)
transp.close()

# should not raise, as after closing a live connection
self.assertIsNone(transp.write(b'data'))
self.assertEqual(transp.get_write_buffer_size(), 0)
new_proto = asyncio.Protocol()
transp.set_protocol(new_proto) # as loop.sendfile()'s fallback does
self.assertIs(transp.get_protocol(), new_proto)
transp.abort()
self.assertTrue(transp.is_closing())

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,7 @@
Fix :exc:`AttributeError` from the methods of an :mod:`asyncio` SSL transport,
such as :meth:`~asyncio.WriteTransport.write` and
:meth:`~asyncio.BaseTransport.set_protocol`, when the transport is closed after
its connection was lost. This also affected the fallback of
:meth:`loop.sendfile() <asyncio.loop.sendfile>` over SSL when the peer went
away mid-transfer. The SSL context is still released once the connection is
lost.
Loading