6363_PREFACE = b"PRI * HTTP/2.0\r \n \r \n SM\r \n \r \n "
6464# An HTTP/2 frame header: 24-bit length, type, flags, stream id (RFC 9113 section 4.1).
6565_FRAME = struct .Struct (">BHBBI" )
66- _DATA , _HEADERS , _RST_STREAM , _SETTINGS , _GOAWAY = 0x0 , 0x1 , 0x3 , 0x4 , 0x7
67- _END_STREAM_FLAG , _ACK_FLAG = 0x1 , 0x1
66+ _DATA , _HEADERS , _RST_STREAM , _SETTINGS , _GOAWAY , _CONTINUATION = 0x0 , 0x1 , 0x3 , 0x4 , 0x7 , 0x9
67+ _END_STREAM_FLAG , _ACK_FLAG , _END_HEADERS_FLAG = 0x1 , 0x1 , 0x4
6868_MAX_CONCURRENT_STREAMS = 0x3
69+ _PUSH_PROMISE = 0x5
70+ _PROTOCOL_ERROR , _CANCEL , _ENHANCE_YOUR_CALM = 0x1 , 0x8 , 0xB
6971# Calls one relayed connection may hold open when the gRPC server advertises no
70- # SETTINGS_MAX_CONCURRENT_STREAMS (grpcio advertises none); past it the face closes the
71- # connection, so what it tracks per connection is bounded.
72+ # SETTINGS_MAX_CONCURRENT_STREAMS (grpcio advertises none unless the service sets
73+ # grpc.max_concurrent_streams, as the scaffold's server does). Past it the face resets
74+ # the call alone: ENHANCE_YOUR_CALM to the client (gRPC's RESOURCE_EXHAUSTED, which a
75+ # client does not retry on its own), and CANCEL to the server. The server still receives
76+ # the call's header block, which keeps the connection's header compression in step, so it
77+ # may have started the call before the cancel: the face never promises it did not. What
78+ # the face tracks per connection is bounded by it. When the server advertises a limit the
79+ # face enforces nothing: the server refuses what is past it (REFUSED_STREAM), truthfully.
7280DEFAULT_MAX_OPEN_CALLS = 1000
81+ # Bytes of frames for the client (refusals, a GOAWAY) the relay holds while the server's
82+ # frame in flight completes; past it the relay reads nothing more from the client until
83+ # they are written, so a client that opens calls faster than it reads them stalls itself.
84+ _MAX_QUEUED_FOR_CLIENT = 16 * 1024
85+ # How long a closing relay waits for the server's frame in flight to complete, so the
86+ # client's GOAWAY lands between two frames.
87+ _GOAWAY_GRACE_S = 5.0
7388# The most of a SETTINGS or GOAWAY payload the relay reads; the rest is skipped.
7489_CONTROL_PAYLOAD = 256
7590_ENVELOPE = struct .Struct (">BI" )
@@ -224,55 +239,107 @@ class _ProtocolError(Exception):
224239 """A client frame no HTTP/2 client may send: the relay closes the connection."""
225240
226241
242+ def _h2_frame (kind : int , flags : int , stream : int , payload : bytes = b"" ) -> bytes :
243+ size = len (payload )
244+ return _FRAME .pack (size >> 16 , size & 0xFFFF , kind , flags , stream ) + payload
245+
246+
247+ def _rst_stream (stream : int , code : int ) -> bytes :
248+ return _h2_frame (_RST_STREAM , 0 , stream , struct .pack (">I" , code ))
249+
250+
251+ def _goaway (last_stream : int , code : int , reason : str ) -> bytes :
252+ return _h2_frame (_GOAWAY , 0 , 0 , struct .pack (">II" , last_stream , code ) + reason .encode ())
253+
254+
227255class _Frames :
228- """The HTTP/2 frames in one direction of a relayed connection, after the preface:
229- ``on_frame(type, flags, stream, payload)`` for each. The payload is read for SETTINGS
230- and GOAWAY only (at most ``_CONTROL_PAYLOAD`` bytes of it) and skipped otherwise."""
256+ """The HTTP/2 frames in one direction of a relayed connection, after the preface.
257+
258+ ``feed`` calls ``on_frame(type, flags, stream, payload)`` for each frame and returns
259+ where frames end in the data, each with the bytes ``on_frame`` returned, to be written
260+ right after that frame in this direction. The payload is read for SETTINGS and GOAWAY
261+ only (at most ``_CONTROL_PAYLOAD`` bytes of it) and skipped otherwise. A
262+ ``_ProtocolError`` from ``on_frame`` stops the feed; it is kept in ``error`` and the
263+ ends before the offending frame are returned."""
231264
232265 def __init__ (self , on_frame ):
233266 self ._on_frame = on_frame
234267 self ._header = bytearray ()
235268 self ._frame = None # (type, flags, stream) while its payload is read
269+ self ._after = b"" # what on_frame returned for it
236270 self ._payload = bytearray ()
237271 self ._keep = 0 # payload bytes still to read
238272 self ._skip = 0 # payload bytes still to skip
239-
240- def feed (self , data : bytes ) -> None :
241- at = 0
242- while at < len (data ):
243- if self ._keep :
244- step = min (self ._keep , len (data ) - at )
245- self ._payload += data [at : at + step ]
246- self ._keep -= step
247- at += step
248- if not self ._keep :
249- self ._emit ()
250- continue
251- if self ._skip :
252- step = min (self ._skip , len (data ) - at )
253- self ._skip -= step
254- at += step
255- continue
256- step = min (_FRAME .size - len (self ._header ), len (data ) - at )
257- self ._header += data [at : at + step ]
258- at += step
259- if len (self ._header ) == _FRAME .size :
260- high , low , kind , flags , stream = _FRAME .unpack (self ._header )
261- self ._header .clear ()
262- length = high << 16 | low
263- self ._frame = (kind , flags , stream & 0x7FFFFFFF )
264- if kind in (_SETTINGS , _GOAWAY ):
265- self ._keep = min (length , _CONTROL_PAYLOAD )
266- self ._skip = length - self ._keep
267- else :
268- self ._skip = length
269- if not self ._keep :
273+ self ._in_block = False # inside a header block: HEADERS ... CONTINUATION
274+ self .error : _ProtocolError | None = None
275+
276+ @property
277+ def at_boundary (self ) -> bool :
278+ """Between two frames, and outside a header block: where another frame may go
279+ (RFC 9113 section 6.10: nothing comes between a HEADERS and its CONTINUATION)."""
280+ return not self ._header and not self ._keep and not self ._skip and not self ._in_block
281+
282+ def feed (self , data : bytes , every_end : bool = False ) -> list [tuple [int , bytes ]]:
283+ """Where frames end in ``data``: each end where ``on_frame`` returned bytes to
284+ write after the frame, and, with ``every_end``, each end where another frame may
285+ go. After a ``_ProtocolError``, the last end is the last place another frame
286+ may go before the offending frame."""
287+ ends : list [tuple [int , bytes ]] = []
288+ at , size = 0 , len (data )
289+ clear = 0 if self .at_boundary else - 1 # the last such place in ``data``
290+ try :
291+ while at < size :
292+ if self ._keep :
293+ step = min (self ._keep , size - at )
294+ self ._payload += data [at : at + step ]
295+ self ._keep -= step
296+ at += step
297+ if self ._keep :
298+ break
270299 self ._emit ()
300+ elif self ._skip :
301+ step = min (self ._skip , size - at )
302+ self ._skip -= step
303+ at += step
304+ if self ._skip :
305+ break
306+ elif self ._header or size - at < _FRAME .size :
307+ step = min (_FRAME .size - len (self ._header ), size - at )
308+ self ._header += data [at : at + step ]
309+ at += step
310+ if len (self ._header ) < _FRAME .size :
311+ break
312+ self ._start (* _FRAME .unpack (self ._header ))
313+ self ._header .clear ()
314+ else : # a whole frame header in this chunk: read it in place
315+ self ._start (* _FRAME .unpack_from (data , at ))
316+ at += _FRAME .size
317+ if not (self ._keep or self ._skip or self ._in_block ):
318+ clear = at
319+ if every_end or self ._after :
320+ ends .append ((at , self ._after ))
321+ self ._after = b""
322+ except _ProtocolError as refusal :
323+ self .error = refusal
324+ if clear > (ends [- 1 ][0 ] if ends else 0 ):
325+ ends .append ((clear , b"" ))
326+ return ends
327+
328+ def _start (self , high , low , kind , flags , stream ):
329+ """A frame header read: what of its payload to keep, and its frame if none."""
330+ length = high << 16 | low
331+ self ._frame = (kind , flags , stream & 0x7FFFFFFF )
332+ if kind in (_HEADERS , _PUSH_PROMISE , _CONTINUATION ):
333+ self ._in_block = not flags & _END_HEADERS_FLAG
334+ self ._keep = min (length , _CONTROL_PAYLOAD ) if kind in (_SETTINGS , _GOAWAY ) else 0
335+ self ._skip = length - self ._keep
336+ if not self ._keep :
337+ self ._emit ()
271338
272339 def _emit (self ):
273340 payload = bytes (self ._payload )
274341 self ._payload .clear ()
275- self ._on_frame (* self ._frame , payload )
342+ self ._after = self . _on_frame (* self ._frame , payload ) or b""
276343
277344
278345class _Connection :
@@ -299,60 +366,133 @@ async def _relay(self, reader, writer, head):
299366
300367 The frame headers say which calls are open: a client HEADERS on a new stream (odd,
301368 above every stream before it) opens one, and the server's END_STREAM, either
302- side's RST_STREAM, or the server's GOAWAY above the stream ends it. A client that
303- opens an even stream, or more calls than the server allows at once
304- (SETTINGS_MAX_CONCURRENT_STREAMS, ``DEFAULT_MAX_OPEN_CALLS`` when it advertises
305- none), is closed: no HTTP/2 client does either."""
369+ side's RST_STREAM, or the server's GOAWAY above the stream ends it. A call past
370+ the server's own SETTINGS_MAX_CONCURRENT_STREAMS is relayed as it came, for the
371+ server to refuse. When the server advertises none, a call past
372+ ``DEFAULT_MAX_OPEN_CALLS`` is reset alone (see there). The face's frames for the
373+ client are written only between two of the server's frames and outside a header
374+ block; while more than ``_MAX_QUEUED_FOR_CLIENT`` bytes of them wait, the client
375+ is not read. A client that opens an even stream, which no HTTP/2 client does, gets
376+ a GOAWAY naming why, and the connection is closed."""
306377 upstream_reader , upstream_writer = await asyncio .open_connection (* self .grpc_address )
307378 upstream_writer .write (head )
308379 loop = asyncio .get_running_loop ()
309380 last = loop .time ()
310381 calls : set [int ] = set ()
311- highest = 0
382+ highest = accepted = 0
383+ refusing = 0 # the refused stream whose header block is still arriving
312384 max_calls = DEFAULT_MAX_OPEN_CALLS
385+ advertised = False # the server sent SETTINGS_MAX_CONCURRENT_STREAMS
386+ to_client = bytearray () # frames for the client, written at a frame boundary
387+ flushed = asyncio .Event () # set when the server pump wrote them
388+ told = False # the face wrote to the client since the client pump last drained
313389
314390 def from_client (kind , flags , stream , payload ):
315- nonlocal highest
316- if kind == _HEADERS :
391+ nonlocal highest , accepted , refusing
392+ if kind == _HEADERS and stream > highest :
317393 if stream % 2 == 0 :
318394 raise _ProtocolError (f"HEADERS on stream { stream } , which a server opens" )
319- if stream <= highest :
320- # Trailers on a call, or a stream the server refuses as closed:
321- # never a new call.
322- return
323395 highest = stream
324- if len (calls ) >= max_calls :
325- raise _ProtocolError (f"more than { max_calls } calls open at once" )
326- calls .add (stream )
396+ if len (calls ) < max_calls :
397+ calls .add (stream )
398+ accepted = stream
399+ elif not advertised :
400+ refusing = stream
401+ tell_client (_rst_stream (stream , _ENHANCE_YOUR_CALM ))
402+ # Past an advertised limit the server refuses the call itself.
403+ elif kind == _HEADERS and stream % 2 == 0 :
404+ raise _ProtocolError (f"HEADERS on stream { stream } , which a server opens" )
327405 elif kind == _RST_STREAM :
328406 calls .discard (stream )
407+ # A lower odd stream id opens nothing: trailers on a call, or a stream the
408+ # server refuses as closed.
409+ if refusing and stream == refusing and kind in (_HEADERS , _CONTINUATION ):
410+ if flags & _END_HEADERS_FLAG :
411+ refusing = 0
412+ return _rst_stream (stream , _CANCEL ) # after its header block
413+ return None
329414
330415 def from_server (kind , flags , stream , payload ):
331- nonlocal max_calls
416+ nonlocal max_calls , advertised
332417 if kind == _RST_STREAM or (kind in (_DATA , _HEADERS ) and flags & _END_STREAM_FLAG ):
333418 calls .discard (stream )
334419 elif kind == _SETTINGS and not flags & _ACK_FLAG :
335420 for at in range (0 , len (payload ) - 5 , 6 ):
336421 key , value = struct .unpack_from (">HI" , payload , at )
337422 if key == _MAX_CONCURRENT_STREAMS :
338- max_calls = value
423+ max_calls , advertised = value , True
339424 elif kind == _GOAWAY and len (payload ) >= 4 :
340425 # Streams above the last one the server processes are never answered.
341426 last_stream = struct .unpack_from (">I" , payload )[0 ] & 0x7FFFFFFF
342427 calls .difference_update ({call for call in calls if call > last_stream })
428+ return None
343429
344- async def pump (source , sink , frames ):
345- nonlocal last
430+ client , server = _Frames (from_client ), _Frames (from_server )
431+
432+ def tell_client (frame ):
433+ nonlocal told
434+ # Only between two of the server's frames: queued while one is half relayed,
435+ # and written by the server pump at that frame's end. Nothing to a client gone.
436+ if writer .is_closing ():
437+ return
438+ if server .at_boundary and not to_client :
439+ writer .write (frame )
440+ told = True
441+ else :
442+ to_client .extend (frame )
443+
444+ async def written ():
445+ """Wait until the server pump has written what is queued for the client."""
446+ while to_client :
447+ flushed .clear ()
448+ await flushed .wait ()
449+
450+ def relay (chunk , sink , frames , ends , queued ):
451+ # As few writes as the frames to insert allow: one per insertion point.
452+ at = 0
453+ for end , after in ends :
454+ if not after and not queued :
455+ continue
456+ sink .write (chunk [at :end ] + after )
457+ at = end
458+ if queued :
459+ sink .write (bytes (queued ))
460+ queued .clear ()
461+ flushed .set ()
462+ stop = len (chunk ) if frames .error is None else (ends [- 1 ][0 ] if ends else at )
463+ if stop > at :
464+ sink .write (chunk [at :stop ])
465+
466+ async def pump (source , sink , frames , queued ):
467+ nonlocal last , told
346468 try :
347469 while chunk := await source .read (65536 ):
470+ if writer .is_closing ():
471+ return # the client is gone: nothing left to relay
348472 last = loop .time ()
349- frames .feed (chunk )
350- sink .write (chunk )
473+ relay (chunk , sink , frames , frames .feed (chunk , bool (queued )), queued )
474+ if frames .error is not None :
475+ _log .info ("connect face: closing a gRPC connection: %s" , frames .error )
476+ tell_client (_goaway (accepted , _PROTOCOL_ERROR , str (frames .error )))
477+ try :
478+ await asyncio .wait_for (written (), _GOAWAY_GRACE_S )
479+ except TimeoutError :
480+ _log .warning (
481+ "connect face: the GOAWAY was not delivered: the server's "
482+ "frame in flight did not complete within %s s" ,
483+ _GOAWAY_GRACE_S ,
484+ )
485+ return
351486 await sink .drain ()
487+ if sink is not writer :
488+ # The refusals: a client must read them before it is read again.
489+ if len (to_client ) >= _MAX_QUEUED_FOR_CLIENT :
490+ await written ()
491+ if told :
492+ told = False
493+ await writer .drain ()
352494 except (ConnectionError , OSError ):
353495 pass
354- except _ProtocolError as refusal :
355- _log .info ("connect face: closing a gRPC connection: %s" , refusal )
356496
357497 async def idle ():
358498 # The clock runs from the last byte either way, and only ends the connection
@@ -364,8 +504,8 @@ async def idle():
364504 await asyncio .sleep (remaining if remaining > 0 else self .idle_timeout )
365505
366506 tasks = (
367- asyncio .ensure_future (pump (reader , upstream_writer , _Frames ( from_client ))),
368- asyncio .ensure_future (pump (upstream_reader , writer , _Frames ( from_server ) )),
507+ asyncio .ensure_future (pump (reader , upstream_writer , client , bytearray ( ))),
508+ asyncio .ensure_future (pump (upstream_reader , writer , server , to_client )),
369509 asyncio .ensure_future (idle ()),
370510 )
371511 try :
0 commit comments