66
77
88import asyncio
9+ import contextlib
910import socket
1011import threading
1112import typing
@@ -34,6 +35,7 @@ def __init__(self, *, listening_addr: str='127.0.0.1',
3435 self .connections = {}
3536 self .sock = None
3637 self .listen_task = None
38+ self .connection_factory = Connection
3739
3840 async def _wait (self , work ):
3941 work_task = asyncio .ensure_future (work )
@@ -98,11 +100,13 @@ async def _main(self, started_event):
98100 try :
99101 await self .listen_task
100102 finally :
103+ tasks = list (self .connections .values ())
101104 for c in list (self .connections ):
102105 c .close ()
103- await asyncio .sleep ( 0.01 )
106+ await asyncio .gather ( * tasks , return_exceptions = True )
104107 if hasattr (self .loop , 'remove_reader' ):
105- self .loop .remove_reader (self .sock .fileno ())
108+ with contextlib .suppress (NotImplementedError ):
109+ self .loop .remove_reader (self .sock .fileno ())
106110 self .sock .close ()
107111
108112 async def listen (self ):
@@ -119,7 +123,7 @@ async def listen(self):
119123 except StopServer :
120124 break
121125
122- conn = Connection (client_sock , backend_sock , self )
126+ conn = self . connection_factory (client_sock , backend_sock , self )
123127 conn_task = self .loop .create_task (conn .handle ())
124128 self .connections [conn ] = conn_task
125129
@@ -141,13 +145,13 @@ def reset(self):
141145 self .restore_connectivity ()
142146
143147 def _close_connection (self , connection ):
144- conn_task = self .connections .pop (connection , None )
148+ conn_task = self .connections .get (connection )
145149 if conn_task is not None :
146150 conn_task .cancel ()
147151
148152 def close_all_connections (self ):
149153 for conn in list (self .connections ):
150- self .loop .call_soon_threadsafe (self . _close_connection , conn )
154+ self .loop .call_soon_threadsafe (conn . close )
151155
152156
153157class Connection :
@@ -161,6 +165,27 @@ def __init__(self, client_sock, backend_sock, proxy):
161165 self .proxy_to_backend_task = None
162166 self .proxy_from_backend_task = None
163167 self .is_closed = False
168+ self .client_reader = None
169+ self .client_writer = None
170+
171+ async def prepare (self ):
172+ """Optionally negotiate or inject faults before forwarding bytes."""
173+ return True
174+
175+ async def use_client_stream (self , ssl_context = None ):
176+ """Wrap the accepted socket in a stream, optionally with direct TLS."""
177+ self .client_reader = asyncio .StreamReader ()
178+ protocol = asyncio .StreamReaderProtocol (self .client_reader )
179+ transport , _ = await self .loop .connect_accepted_socket (
180+ lambda : protocol , self .client_sock , ssl = ssl_context )
181+ self .client_writer = asyncio .StreamWriter (
182+ transport , protocol , self .client_reader , self .loop )
183+
184+ async def start_tls (self , ssl_context ):
185+ writer = self .client_writer
186+ writer ._transport = await self .loop .start_tls (
187+ writer .transport , writer ._protocol , ssl_context , server_side = True )
188+ writer ._protocol ._over_ssl = True
164189
165190 def close (self ):
166191 if self .is_closed :
@@ -170,48 +195,72 @@ def close(self):
170195
171196 if self .proxy_to_backend_task is not None :
172197 self .proxy_to_backend_task .cancel ()
173- self .proxy_to_backend_task = None
174198
175199 if self .proxy_from_backend_task is not None :
176200 self .proxy_from_backend_task .cancel ()
177- self .proxy_from_backend_task = None
178201
179202 self .proxy ._close_connection (self )
180203
181204 async def handle (self ):
182- self .proxy_to_backend_task = asyncio .ensure_future (
183- self .proxy_to_backend ())
205+ try :
206+ if not await self .prepare ():
207+ return
184208
185- self .proxy_from_backend_task = asyncio .ensure_future (
186- self .proxy_from_backend ())
209+ self .proxy_to_backend_task = asyncio .ensure_future (
210+ self .proxy_to_backend ())
211+ self .proxy_from_backend_task = asyncio .ensure_future (
212+ self .proxy_from_backend ())
187213
188- try :
189214 await asyncio .wait (
190215 [self .proxy_to_backend_task , self .proxy_from_backend_task ],
191216 return_when = asyncio .FIRST_COMPLETED )
192217
218+ except ConnectionError :
219+ pass
220+
193221 finally :
222+ # Relay completion can schedule close() while cleanup is awaiting
223+ # its children. Do not let that callback cancel cleanup itself.
224+ self .is_closed = True
194225 if self .proxy_to_backend_task is not None :
195226 self .proxy_to_backend_task .cancel ()
196227
197228 if self .proxy_from_backend_task is not None :
198229 self .proxy_from_backend_task .cancel ()
199230
231+ await asyncio .gather (* (
232+ task for task in (self .proxy_to_backend_task ,
233+ self .proxy_from_backend_task )
234+ if task is not None
235+ ), return_exceptions = True )
236+
200237 # Asyncio fails to properly remove the readers and writers
201238 # when the task doing recv() or send() is cancelled, so
202239 # we must remove the readers and writers manually before
203240 # closing the sockets.
204- self .loop .remove_reader (self .client_sock .fileno ())
205- self .loop .remove_writer (self .client_sock .fileno ())
206- self .loop .remove_reader (self .backend_sock .fileno ())
207- self .loop .remove_writer (self .backend_sock .fileno ())
208-
209- self .client_sock .close ()
210- self .backend_sock .close ()
241+ sockets = [self .backend_sock ]
242+ if self .client_writer is None :
243+ sockets .append (self .client_sock )
244+ else :
245+ self .client_writer .close ()
246+ with contextlib .suppress (ConnectionError ):
247+ await self .client_writer .wait_closed ()
248+ for sock in sockets :
249+ if sock .fileno () < 0 :
250+ continue
251+ # ProactorEventLoop has no reader/writer registration API.
252+ with contextlib .suppress (NotImplementedError ):
253+ self .loop .remove_reader (sock .fileno ())
254+ self .loop .remove_writer (sock .fileno ())
255+ sock .close ()
256+ self .proxy .connections .pop (self , None )
211257
212258 async def _read (self , sock , n ):
213- read_task = asyncio .ensure_future (
214- self .loop .sock_recv (sock , n ))
259+ if sock is self .client_sock and self .client_reader is not None :
260+ read = self .client_reader .read (n )
261+ else :
262+ read = self .loop .sock_recv (sock , n )
263+ read_task = asyncio .ensure_future (read )
215264 conn_event_task = asyncio .ensure_future (
216265 self .connectivity_loss .wait ())
217266
@@ -230,10 +279,16 @@ async def _read(self, sock, n):
230279 read_task .cancel ()
231280 if not conn_event_task .done ():
232281 conn_event_task .cancel ()
282+ await asyncio .gather (read_task , conn_event_task ,
283+ return_exceptions = True )
233284
234285 async def _write (self , sock , data ):
235- write_task = asyncio .ensure_future (
236- self .loop .sock_sendall (sock , data ))
286+ if sock is self .client_sock and self .client_writer is not None :
287+ self .client_writer .write (data )
288+ write = self .client_writer .drain ()
289+ else :
290+ write = self .loop .sock_sendall (sock , data )
291+ write_task = asyncio .ensure_future (write )
237292 conn_event_task = asyncio .ensure_future (
238293 self .connectivity_loss .wait ())
239294
@@ -252,6 +307,8 @@ async def _write(self, sock, data):
252307 write_task .cancel ()
253308 if not conn_event_task .done ():
254309 conn_event_task .cancel ()
310+ await asyncio .gather (write_task , conn_event_task ,
311+ return_exceptions = True )
255312
256313 async def proxy_to_backend (self ):
257314 buf = None
0 commit comments