Skip to content

Commit fc2a6cf

Browse files
miss-islingtonkumaraditya303kasimov-maxim
authored
[3.14] gh-142352: Fix asyncio start_tls() to transfer buffered data from StreamReader (GH-142354) (#145363)
gh-142352: Fix `asyncio` `start_tls()` to transfer buffered data from StreamReader (GH-142354) (cherry picked from commit 0598f4a) Co-authored-by: Kumar Aditya <kumaraditya@python.org> Co-authored-by: Maksym Kasimov <39828623+kasimov-maxim@users.noreply.github.com>
1 parent d76c56e commit fc2a6cf

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
@@ -1345,6 +1345,17 @@ async def start_tls(self, transport, protocol, sslcontext, *,
13451345
# have a chance to get called before "ssl_protocol.connection_made()".
13461346
transport.pause_reading()
13471347

1348+
# gh-142352: move buffered StreamReader data to SSLProtocol
1349+
if server_side:
1350+
from .streams import StreamReaderProtocol
1351+
if isinstance(protocol, StreamReaderProtocol):
1352+
stream_reader = getattr(protocol, '_stream_reader', None)
1353+
if stream_reader is not None:
1354+
buffer = stream_reader._buffer
1355+
if buffer:
1356+
ssl_protocol._incoming.write(buffer)
1357+
buffer.clear()
1358+
13481359
transport.set_protocol(ssl_protocol)
13491360
conmade_cb = self.call_soon(ssl_protocol.connection_made, transport)
13501361
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
@@ -819,6 +819,48 @@ async def client(addr):
819819
self.assertEqual(msg1, b"hello world 1!\n")
820820
self.assertEqual(msg2, b"hello world 2!\n")
821821

822+
@unittest.skipIf(ssl is None, 'No ssl module')
823+
def test_start_tls_buffered_data(self):
824+
# gh-142352: test start_tls() with buffered data
825+
826+
async def server_handler(client_reader, client_writer):
827+
# Wait for TLS ClientHello to be buffered before start_tls().
828+
await client_reader._wait_for_data('test_start_tls_buffered_data'),
829+
self.assertTrue(client_reader._buffer)
830+
await client_writer.start_tls(test_utils.simple_server_sslcontext())
831+
832+
line = await client_reader.readline()
833+
self.assertEqual(line, b"ping\n")
834+
client_writer.write(b"pong\n")
835+
await client_writer.drain()
836+
client_writer.close()
837+
await client_writer.wait_closed()
838+
839+
async def client(addr):
840+
reader, writer = await asyncio.open_connection(*addr)
841+
await writer.start_tls(test_utils.simple_client_sslcontext())
842+
843+
writer.write(b"ping\n")
844+
await writer.drain()
845+
line = await reader.readline()
846+
self.assertEqual(line, b"pong\n")
847+
writer.close()
848+
await writer.wait_closed()
849+
850+
async def run_test():
851+
server = await asyncio.start_server(
852+
server_handler, socket_helper.HOSTv4, 0)
853+
server_addr = server.sockets[0].getsockname()
854+
855+
await client(server_addr)
856+
server.close()
857+
await server.wait_closed()
858+
859+
messages = []
860+
self.loop.set_exception_handler(lambda loop, ctx: messages.append(ctx))
861+
self.loop.run_until_complete(run_test())
862+
self.assertEqual(messages, [])
863+
822864
def test_streamreader_constructor_without_loop(self):
823865
with self.assertRaisesRegex(RuntimeError, 'no current event loop'):
824866
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)