Skip to content

Commit aa0aa31

Browse files
[3.13] gh-142352: Fix asyncio start_tls() to transfer buffered data from StreamReader (GH-142354) (#145364)
[3.13] gh-142352: Fix `asyncio` `start_tls()` to transfer buffered data from StreamReader (GH-142354) (cherry picked from commit 0598f4a) Co-authored-by: Maksym Kasimov <39828623+kasimov-maxim@users.noreply.github.com>
1 parent 7f9c369 commit aa0aa31

File tree

3 files changed

+57
-0
lines changed

3 files changed

+57
-0
lines changed

Lib/asyncio/base_events.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,17 @@ async def start_tls(self, transport, protocol, sslcontext, *,
13481348
# have a chance to get called before "ssl_protocol.connection_made()".
13491349
transport.pause_reading()
13501350

1351+
# gh-142352: move buffered StreamReader data to SSLProtocol
1352+
if server_side:
1353+
from .streams import StreamReaderProtocol
1354+
if isinstance(protocol, StreamReaderProtocol):
1355+
stream_reader = getattr(protocol, '_stream_reader', None)
1356+
if stream_reader is not None:
1357+
buffer = stream_reader._buffer
1358+
if buffer:
1359+
ssl_protocol._incoming.write(buffer)
1360+
buffer.clear()
1361+
13511362
transport.set_protocol(ssl_protocol)
13521363
conmade_cb = self.call_soon(ssl_protocol.connection_made, transport)
13531364
resume_cb = self.call_soon(transport.resume_reading)

Lib/test/test_asyncio/test_streams.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,48 @@ def test_read_all_from_pipe_reader(self):
868868
data = self.loop.run_until_complete(reader.read(-1))
869869
self.assertEqual(data, b'data')
870870

871+
@unittest.skipIf(ssl is None, 'No ssl module')
872+
def test_start_tls_buffered_data(self):
873+
# gh-142352: test start_tls() with buffered data
874+
875+
async def server_handler(client_reader, client_writer):
876+
# Wait for TLS ClientHello to be buffered before start_tls().
877+
await client_reader._wait_for_data('test_start_tls_buffered_data'),
878+
self.assertTrue(client_reader._buffer)
879+
await client_writer.start_tls(test_utils.simple_server_sslcontext())
880+
881+
line = await client_reader.readline()
882+
self.assertEqual(line, b"ping\n")
883+
client_writer.write(b"pong\n")
884+
await client_writer.drain()
885+
client_writer.close()
886+
await client_writer.wait_closed()
887+
888+
async def client(addr):
889+
reader, writer = await asyncio.open_connection(*addr)
890+
await writer.start_tls(test_utils.simple_client_sslcontext())
891+
892+
writer.write(b"ping\n")
893+
await writer.drain()
894+
line = await reader.readline()
895+
self.assertEqual(line, b"pong\n")
896+
writer.close()
897+
await writer.wait_closed()
898+
899+
async def run_test():
900+
server = await asyncio.start_server(
901+
server_handler, socket_helper.HOSTv4, 0)
902+
server_addr = server.sockets[0].getsockname()
903+
904+
await client(server_addr)
905+
server.close()
906+
await server.wait_closed()
907+
908+
messages = []
909+
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
910+
self.loop.run_until_complete(run_test())
911+
self.assertEqual(messages, [])
912+
871913
def test_streamreader_constructor_without_loop(self):
872914
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
873915
asyncio.StreamReader()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :meth:`asyncio.StreamWriter.start_tls` to transfer buffered data from
2+
:class:`~asyncio.StreamReader` to the SSL layer, preventing data loss when
3+
upgrading a connection to TLS mid-stream (e.g., when implementing PROXY
4+
protocol support).

0 commit comments

Comments
 (0)