@@ -84,8 +84,24 @@ def send_connect_request(sock, destination):
8484}
8585
8686
87+ def _ssl_pending (sock ):
88+ """Bytes already decrypted and waiting in the OpenSSL read buffer (may be non-empty)."""
89+ return sock .pending () if isinstance (sock , ssl .SSLSocket ) else 0
90+
91+
8792def wait_for_response (sock , conn , stream_id ):
93+ """Wait for CONNECT response.
94+
95+ Uses SSLSocket.pending(): we only count fast recv iterations (draining the OpenSSL read
96+ buffer without blocking on the network) toward a livelock limit.
97+ """
98+ fast = 0
99+ max_fast = 100_000
88100 while True :
101+ pending_before = _ssl_pending (sock )
102+ if pending_before == 0 :
103+ fast = 0
104+
89105 data = sock .recv (65535 )
90106 if not data :
91107 raise ConnectionError ("Proxy closed the connection before responding" )
@@ -94,6 +110,10 @@ def wait_for_response(sock, conn, stream_id):
94110 sock .sendall (conn .data_to_send ())
95111
96112 for event in events :
113+ if isinstance (event , h2 .events .ConnectionTerminated ):
114+ raise ConnectionError (
115+ f"Proxy closed HTTP/2 connection: error_code={ event .error_code !r} "
116+ )
97117 if isinstance (event , h2 .events .ResponseReceived ) and event .stream_id == stream_id :
98118 headers = dict (event .headers )
99119 status = int (headers [":status" ])
@@ -102,25 +122,36 @@ def wait_for_response(sock, conn, stream_id):
102122 if isinstance (event , h2 .events .StreamReset ):
103123 raise ConnectionError (f"Stream reset by proxy: error code { event .error_code } " )
104124
125+ if pending_before > 0 :
126+ fast += 1
127+ if fast >= max_fast :
128+ raise ConnectionError (
129+ "Timed out waiting for CONNECT response (excessive HTTP/2 traffic without response)"
130+ )
131+
105132
106133class H2Tunnel :
107134 """Bidirectional byte pipe over an HTTP/2 DATA stream."""
108135
136+ # If we keep receiving TLS/plaintext without ever getting DATA for this stream (or EOF),
137+ # something is wrong — bail out instead of burning CPU forever.
138+ _MAX_FAST_RECV_WITHOUT_APP_DATA = 100_000
139+
109140 def __init__ (self , sock , h2_conn , stream_id ):
110141 self ._sock = sock
111142 self ._conn = h2_conn
112143 self ._sid = stream_id
113144 self ._buffer = b""
114145 self ._eof = False
115146
116- def _read_from_tunnel (self ):
117- raw = self ._sock .recv (65535 )
118- if not raw :
119- self ._eof = True
120- return
147+ def _process_raw (self , raw ):
121148 events = self ._conn .receive_data (raw )
122149 self ._sock .sendall (self ._conn .data_to_send ())
123150 for ev in events :
151+ if isinstance (ev , h2 .events .ConnectionTerminated ):
152+ raise ConnectionError (
153+ f"Proxy closed HTTP/2 connection: error_code={ ev .error_code !r} "
154+ )
124155 if isinstance (ev , h2 .events .DataReceived ) and ev .stream_id == self ._sid :
125156 self ._conn .acknowledge_received_data (ev .flow_controlled_length , ev .stream_id )
126157 self ._sock .sendall (self ._conn .data_to_send ())
@@ -129,8 +160,26 @@ def _read_from_tunnel(self):
129160 self ._eof = True
130161
131162 def recv (self , bufsize ):
163+ fast = 0
132164 while not self ._buffer and not self ._eof :
133- self ._read_from_tunnel ()
165+ pending_before = _ssl_pending (self ._sock )
166+ if pending_before == 0 :
167+ fast = 0
168+
169+ raw = self ._sock .recv (65535 )
170+ if not raw :
171+ self ._eof = True
172+ break
173+
174+ self ._process_raw (raw )
175+
176+ if not self ._buffer and not self ._eof and pending_before > 0 :
177+ fast += 1
178+ if fast >= self ._MAX_FAST_RECV_WITHOUT_APP_DATA :
179+ raise ConnectionError (
180+ "HTTP/2 tunnel livelock: received many frames but no payload for CONNECT stream"
181+ )
182+
134183 if not self ._buffer :
135184 return b""
136185 out = self ._buffer [:bufsize ]
@@ -167,6 +216,10 @@ def tunnel_tls_handshake(tunnel, dest_host, dest_ca=None):
167216 if not data :
168217 raise ConnectionError (f"tunnel closed during TLS handshake with { dest_host } " )
169218 incoming_bio .write (data )
219+ except ssl .SSLWantWriteError :
220+ out = outgoing_bio .read ()
221+ if out :
222+ tunnel .send (out )
170223
171224 out = outgoing_bio .read ()
172225 if out :
@@ -175,8 +228,25 @@ def tunnel_tls_handshake(tunnel, dest_host, dest_ca=None):
175228 return ssl_obj , incoming_bio , outgoing_bio
176229
177230
178- def ssl_send (ssl_obj , outgoing_bio , tunnel , data ):
179- ssl_obj .write (data )
231+ def ssl_send (ssl_obj , incoming_bio , outgoing_bio , tunnel , data ):
232+ view = memoryview (data )
233+ while len (view ):
234+ try :
235+ n = ssl_obj .write (view )
236+ view = view [n :]
237+ except ssl .SSLWantReadError :
238+ out = outgoing_bio .read ()
239+ if out :
240+ tunnel .send (out )
241+ chunk = tunnel .recv (65535 )
242+ if not chunk :
243+ raise ConnectionError ("tunnel closed while sending to destination TLS" )
244+ incoming_bio .write (chunk )
245+ except ssl .SSLWantWriteError :
246+ out = outgoing_bio .read ()
247+ if out :
248+ tunnel .send (out )
249+
180250 out = outgoing_bio .read ()
181251 if out :
182252 tunnel .send (out )
@@ -200,6 +270,10 @@ def ssl_recv(ssl_obj, incoming_bio, outgoing_bio, tunnel, bufsize=4096):
200270 if not data :
201271 break
202272 incoming_bio .write (data )
273+ except ssl .SSLWantWriteError :
274+ out = outgoing_bio .read ()
275+ if out :
276+ tunnel .send (out )
203277 except ssl .SSLZeroReturnError :
204278 break
205279 return b"" .join (chunks )
@@ -212,7 +286,7 @@ def send_get_request(ssl_obj, incoming_bio, outgoing_bio, tunnel, host):
212286 f"Connection: close\r \n "
213287 f"\r \n "
214288 ).encode ()
215- ssl_send (ssl_obj , outgoing_bio , tunnel , request )
289+ ssl_send (ssl_obj , incoming_bio , outgoing_bio , tunnel , request )
216290
217291 response_parts = []
218292 while True :
0 commit comments