tigrcorn-protocols 0.3.16.dev5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. tigrcorn_protocols/__init__.py +1 -0
  2. tigrcorn_protocols/_compression.py +219 -0
  3. tigrcorn_protocols/connect.py +107 -0
  4. tigrcorn_protocols/content_coding.py +179 -0
  5. tigrcorn_protocols/custom/__init__.py +3 -0
  6. tigrcorn_protocols/custom/adapters.py +18 -0
  7. tigrcorn_protocols/custom/registry.py +15 -0
  8. tigrcorn_protocols/flow/__init__.py +1 -0
  9. tigrcorn_protocols/flow/backpressure.py +17 -0
  10. tigrcorn_protocols/flow/buffers.py +29 -0
  11. tigrcorn_protocols/flow/credits.py +21 -0
  12. tigrcorn_protocols/flow/keepalive.py +85 -0
  13. tigrcorn_protocols/flow/timeouts.py +17 -0
  14. tigrcorn_protocols/flow/watermarks.py +16 -0
  15. tigrcorn_protocols/http1/__init__.py +16 -0
  16. tigrcorn_protocols/http1/keepalive.py +21 -0
  17. tigrcorn_protocols/http1/parser.py +481 -0
  18. tigrcorn_protocols/http1/serializer.py +198 -0
  19. tigrcorn_protocols/http1/state.py +9 -0
  20. tigrcorn_protocols/http2/__init__.py +16 -0
  21. tigrcorn_protocols/http2/codec.py +266 -0
  22. tigrcorn_protocols/http2/flow.py +35 -0
  23. tigrcorn_protocols/http2/handler.py +1303 -0
  24. tigrcorn_protocols/http2/hpack.py +393 -0
  25. tigrcorn_protocols/http2/state.py +226 -0
  26. tigrcorn_protocols/http2/streams.py +76 -0
  27. tigrcorn_protocols/http2/websocket.py +360 -0
  28. tigrcorn_protocols/http3/__init__.py +82 -0
  29. tigrcorn_protocols/http3/codec.py +148 -0
  30. tigrcorn_protocols/http3/handler/__init__.py +3 -0
  31. tigrcorn_protocols/http3/handler/core.py +1823 -0
  32. tigrcorn_protocols/http3/handler/webtransport.py +184 -0
  33. tigrcorn_protocols/http3/handler.py +3 -0
  34. tigrcorn_protocols/http3/qpack.py +843 -0
  35. tigrcorn_protocols/http3/state.py +129 -0
  36. tigrcorn_protocols/http3/streams.py +657 -0
  37. tigrcorn_protocols/http3/websocket.py +360 -0
  38. tigrcorn_protocols/lifespan/__init__.py +3 -0
  39. tigrcorn_protocols/lifespan/driver.py +83 -0
  40. tigrcorn_protocols/py.typed +1 -0
  41. tigrcorn_protocols/rawframed/__init__.py +5 -0
  42. tigrcorn_protocols/rawframed/codec.py +18 -0
  43. tigrcorn_protocols/rawframed/frames.py +28 -0
  44. tigrcorn_protocols/rawframed/handler.py +72 -0
  45. tigrcorn_protocols/rawframed/state.py +9 -0
  46. tigrcorn_protocols/registry.py +22 -0
  47. tigrcorn_protocols/scheduler/__init__.py +17 -0
  48. tigrcorn_protocols/scheduler/cancellation.py +40 -0
  49. tigrcorn_protocols/scheduler/dispatch.py +27 -0
  50. tigrcorn_protocols/scheduler/fairness.py +21 -0
  51. tigrcorn_protocols/scheduler/policy.py +12 -0
  52. tigrcorn_protocols/scheduler/priorities.py +8 -0
  53. tigrcorn_protocols/scheduler/quotas.py +19 -0
  54. tigrcorn_protocols/scheduler/runtime.py +156 -0
  55. tigrcorn_protocols/scheduler/tasks.py +31 -0
  56. tigrcorn_protocols/sessions/__init__.py +1 -0
  57. tigrcorn_protocols/sessions/base.py +16 -0
  58. tigrcorn_protocols/sessions/connection.py +12 -0
  59. tigrcorn_protocols/sessions/limits.py +12 -0
  60. tigrcorn_protocols/sessions/manager.py +31 -0
  61. tigrcorn_protocols/sessions/metadata.py +10 -0
  62. tigrcorn_protocols/sessions/quic.py +14 -0
  63. tigrcorn_protocols/streams/__init__.py +1 -0
  64. tigrcorn_protocols/streams/base.py +13 -0
  65. tigrcorn_protocols/streams/ids.py +5 -0
  66. tigrcorn_protocols/streams/multiplex.py +6 -0
  67. tigrcorn_protocols/streams/registry.py +22 -0
  68. tigrcorn_protocols/streams/singleplex.py +6 -0
  69. tigrcorn_protocols/websocket/__init__.py +1 -0
  70. tigrcorn_protocols/websocket/codec.py +31 -0
  71. tigrcorn_protocols/websocket/extensions.py +324 -0
  72. tigrcorn_protocols/websocket/frames.py +174 -0
  73. tigrcorn_protocols/websocket/handler.py +462 -0
  74. tigrcorn_protocols/websocket/handshake.py +66 -0
  75. tigrcorn_protocols/websocket/state.py +10 -0
  76. tigrcorn_protocols-0.3.16.dev5.dist-info/METADATA +240 -0
  77. tigrcorn_protocols-0.3.16.dev5.dist-info/RECORD +80 -0
  78. tigrcorn_protocols-0.3.16.dev5.dist-info/WHEEL +5 -0
  79. tigrcorn_protocols-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
  80. tigrcorn_protocols-0.3.16.dev5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1303 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import suppress
5
+ from urllib.parse import urlsplit
6
+
7
+ from tigrcorn_asgi.receive import HTTPRequestReceive, apply_request_trailer_policy
8
+ from tigrcorn_asgi.scopes.http import build_http_scope
9
+ from tigrcorn_asgi.send import HTTPResponseCollector, iter_response_body_segments, response_body_segments_have_bytes
10
+ from tigrcorn_config.model import ServerConfig
11
+ from tigrcorn_protocols.flow.keepalive import KeepAlivePolicy, KeepAliveRuntime
12
+ from tigrcorn_core.constants import H2_PREFACE
13
+ from tigrcorn_core.errors import ProtocolError
14
+ from tigrcorn_observability.metrics import Metrics
15
+ from tigrcorn_observability.logging import AccessLogger
16
+ from tigrcorn_http.alt_svc import configured_alt_svc_values
17
+ from tigrcorn_http.entity import apply_response_entity_semantics, plan_file_backed_response_entity_semantics
18
+ from tigrcorn_protocols.http1.parser import ParsedRequest
19
+ from tigrcorn_protocols.http2.codec import (
20
+ DEFAULT_SETTINGS,
21
+ H2_CONNECT_ERROR,
22
+ FLAG_ACK,
23
+ FLAG_END_HEADERS,
24
+ FLAG_END_STREAM,
25
+ FRAME_CONTINUATION,
26
+ FRAME_DATA,
27
+ FRAME_GOAWAY,
28
+ FRAME_HEADERS,
29
+ FRAME_PING,
30
+ FRAME_PRIORITY,
31
+ FRAME_PUSH_PROMISE,
32
+ FRAME_RST_STREAM,
33
+ FRAME_SETTINGS,
34
+ FRAME_WINDOW_UPDATE,
35
+ FrameBuffer,
36
+ FrameWriter,
37
+ HTTP2Frame,
38
+ decode_settings,
39
+ headers_payload_fragment,
40
+ parse_goaway,
41
+ parse_priority,
42
+ parse_push_promise,
43
+ parse_window_update,
44
+ serialize_goaway,
45
+ serialize_ping,
46
+ serialize_push_promise,
47
+ serialize_rst_stream,
48
+ serialize_settings,
49
+ serialize_settings_ack,
50
+ SETTING_ENABLE_CONNECT_PROTOCOL,
51
+ SETTING_ENABLE_PUSH,
52
+ SETTING_INITIAL_WINDOW_SIZE,
53
+ SETTING_MAX_CONCURRENT_STREAMS,
54
+ SETTING_MAX_FRAME_SIZE,
55
+ SETTING_MAX_HEADER_LIST_SIZE,
56
+ serialize_window_update,
57
+ strip_padding,
58
+ )
59
+ from tigrcorn_protocols.http2.flow import FlowWaiter, next_adaptive_window_target
60
+ from tigrcorn_protocols.http2.hpack import HPACKDecoder, HPACKEncoder
61
+ from tigrcorn_protocols.http2.state import H2ConnectionState, H2StreamLifecycle, H2StreamState
62
+ from tigrcorn_protocols.scheduler.runtime import ProductionScheduler, WorkLease
63
+ from tigrcorn_protocols.http2.streams import H2StreamRegistry
64
+ from tigrcorn_protocols.connect import close_tcp_writer, half_close_tcp_writer, is_connect_allowed, parse_connect_authority
65
+ from tigrcorn_protocols.http2.websocket import H2WebSocketSession
66
+ from tigrcorn_core.types import ASGIApp
67
+ from tigrcorn_core.utils.authority import authority_allowed
68
+ from tigrcorn_core.utils.headers import apply_response_header_policy, sanitize_early_hints_headers, strip_connection_specific_headers
69
+
70
+
71
+ class _HTTP2ConnectTunnel:
72
+ def __init__(
73
+ self,
74
+ *,
75
+ handler: HTTP2ConnectionHandler,
76
+ stream_id: int,
77
+ authority: str,
78
+ upstream_reader: asyncio.StreamReader,
79
+ upstream_writer: asyncio.StreamWriter,
80
+ work_lease: WorkLease | None = None,
81
+ ) -> None:
82
+ self.handler = handler
83
+ self.stream_id = stream_id
84
+ self.authority = authority
85
+ self.upstream_reader = upstream_reader
86
+ self.upstream_writer = upstream_writer
87
+ self.work_lease = work_lease
88
+ self.relay_task: asyncio.Task[None] | None = None
89
+ self.client_input_closed = False
90
+ self.server_output_closed = False
91
+ self.closed = False
92
+
93
+ async def start(self) -> None:
94
+ try:
95
+ await self.handler._send_stream_headers(self.stream_id, 200, [], end_stream=False)
96
+ except Exception:
97
+ await close_tcp_writer(self.upstream_writer)
98
+ raise
99
+ self.relay_task = asyncio.create_task(
100
+ self._relay_upstream_to_client(),
101
+ name=f'tigrcorn-h2-connect-{self.stream_id}',
102
+ )
103
+
104
+ async def feed_client_data(self, data: bytes, *, end_stream: bool) -> None:
105
+ if self.closed:
106
+ return
107
+ try:
108
+ if data:
109
+ self.upstream_writer.write(data)
110
+ await self.upstream_writer.drain()
111
+ if end_stream and not self.client_input_closed:
112
+ self.client_input_closed = True
113
+ await half_close_tcp_writer(self.upstream_writer)
114
+ except Exception:
115
+ await self.handler._reset_connect_stream(self.stream_id)
116
+ await self.abort()
117
+ return
118
+ await self._finish_if_complete()
119
+
120
+ async def abort(self) -> None:
121
+ if self.closed:
122
+ return
123
+ self.closed = True
124
+ current = asyncio.current_task()
125
+ if self.relay_task is not None and self.relay_task is not current:
126
+ self.relay_task.cancel()
127
+ with suppress(asyncio.CancelledError):
128
+ await self.relay_task
129
+ state = self.handler.streams.find(self.stream_id)
130
+ if state is not None and state.connect_tunnel is self:
131
+ state.connect_tunnel = None
132
+ if self.work_lease is not None:
133
+ self.work_lease.release()
134
+ await close_tcp_writer(self.upstream_writer)
135
+ self.handler._finalize_stream_if_complete(self.stream_id)
136
+
137
+ async def _relay_upstream_to_client(self) -> None:
138
+ reset_stream = False
139
+ try:
140
+ while True:
141
+ chunk = await asyncio.wait_for(self.upstream_reader.read(65536), timeout=self.handler.config.http.idle_timeout)
142
+ if not chunk:
143
+ break
144
+ await self.handler._send_stream_data(self.stream_id, chunk, end_stream=False)
145
+ except asyncio.CancelledError:
146
+ raise
147
+ except Exception:
148
+ reset_stream = True
149
+ else:
150
+ try:
151
+ await self.handler._send_stream_data(self.stream_id, b'', end_stream=True)
152
+ except Exception:
153
+ pass
154
+ finally:
155
+ self.server_output_closed = True
156
+ if reset_stream:
157
+ with suppress(Exception):
158
+ await self.handler._reset_connect_stream(self.stream_id)
159
+ await self._finish_if_complete()
160
+
161
+ async def _finish_if_complete(self) -> None:
162
+ if self.client_input_closed and self.server_output_closed:
163
+ await self.abort()
164
+
165
+
166
+ class HTTP2ConnectionHandler:
167
+ def __init__(
168
+ self,
169
+ *,
170
+ app: ASGIApp,
171
+ config: ServerConfig,
172
+ access_logger: AccessLogger,
173
+ scheduler: ProductionScheduler | None = None,
174
+ metrics: Metrics | None = None,
175
+ reader: asyncio.StreamReader,
176
+ writer: asyncio.StreamWriter,
177
+ client: tuple[str, int] | None,
178
+ server: tuple[str, int] | tuple[str, None] | None,
179
+ scheme: str,
180
+ prebuffer: bytes = b"",
181
+ scope_extensions: dict | None = None,
182
+ ) -> None:
183
+ self.app = app
184
+ self.config = config
185
+ self.access_logger = access_logger
186
+ self.scheduler = scheduler
187
+ self.metrics = metrics
188
+ self.reader = reader
189
+ self.writer = writer
190
+ self.client = client
191
+ self.server = server
192
+ self.scheme = scheme
193
+ self.prebuffer = prebuffer
194
+ self.scope_extensions = dict(scope_extensions or {})
195
+ self.state = H2ConnectionState()
196
+ self.state.local_settings[SETTING_MAX_CONCURRENT_STREAMS] = self.config.http.http2_max_concurrent_streams
197
+ self.state.local_settings[SETTING_MAX_HEADER_LIST_SIZE] = self.config.http.http2_max_headers_size
198
+ self.state.local_settings[SETTING_MAX_FRAME_SIZE] = self.config.http.http2_max_frame_size
199
+ self.state.local_settings[SETTING_INITIAL_WINDOW_SIZE] = self.config.http.http2_initial_stream_window_size
200
+ self.state.connection_receive_window_target = self.config.http.http2_initial_connection_window_size
201
+ self._initial_connection_window_increment = max(
202
+ 0,
203
+ self.state.connection_receive_window_target - DEFAULT_SETTINGS[SETTING_INITIAL_WINDOW_SIZE],
204
+ )
205
+ if self._initial_connection_window_increment:
206
+ self.state.connection_receive_window.increase(self._initial_connection_window_increment)
207
+ self.streams = H2StreamRegistry()
208
+ self.stream_tasks: dict[int, asyncio.Task[None]] = {}
209
+ self.stream_work_leases: dict[int, WorkLease] = {}
210
+ self.frame_buffer = FrameBuffer()
211
+ self.frame_writer = FrameWriter(self.state.max_frame_size)
212
+ self.writer_lock = asyncio.Lock()
213
+ self.waiters: dict[int, FlowWaiter] = {}
214
+ self.hpack_decoder = HPACKDecoder(
215
+ max_table_size=DEFAULT_SETTINGS[0x1],
216
+ max_header_list_size=self.state.max_header_list_size,
217
+ max_header_block_size=self.config.http.http2_max_headers_size,
218
+ )
219
+ self.hpack_encoder = HPACKEncoder(max_table_size=DEFAULT_SETTINGS[0x1])
220
+ self.keepalive_policy = KeepAlivePolicy(
221
+ idle_timeout=self.config.http.idle_timeout,
222
+ ping_interval=self.config.http.http2_keep_alive_interval,
223
+ ping_timeout=self.config.http.http2_keep_alive_timeout,
224
+ )
225
+ self.keepalive = KeepAliveRuntime(self.keepalive_policy) if self.keepalive_policy.enabled else None
226
+ self.keepalive_task: asyncio.Task[None] | None = None
227
+ self.running = True
228
+ self._continuation_stream_id: int | None = None
229
+
230
+ def _record_keepalive_activity(self) -> None:
231
+ if self.keepalive is not None:
232
+ self.keepalive.record_activity()
233
+
234
+ async def _keepalive_loop(self) -> None:
235
+ while self.running and not self.writer.is_closing():
236
+ await asyncio.sleep(0.05)
237
+ if self.keepalive is None or not self.running:
238
+ return
239
+ if not self.state.remote_settings_seen:
240
+ continue
241
+ if self.keepalive.ping_timed_out():
242
+ self.running = False
243
+ self.writer.close()
244
+ with suppress(Exception):
245
+ await self.writer.wait_closed()
246
+ return
247
+ payload = self.keepalive.next_ping_payload()
248
+ if payload is None:
249
+ continue
250
+ await self._write_raw(serialize_ping(payload, ack=False), record_activity=False)
251
+
252
+ async def handle(self) -> None:
253
+ await self._ensure_preface()
254
+ try:
255
+ await self._write_raw(serialize_settings(self.state.local_settings))
256
+ if self._initial_connection_window_increment:
257
+ await self._write_raw(serialize_window_update(0, self._initial_connection_window_increment))
258
+ if self.keepalive is not None:
259
+ self.keepalive_task = asyncio.create_task(self._keepalive_loop(), name='tigrcorn-h2-keepalive')
260
+ while self.running:
261
+ if self._should_finish_after_peer_goaway():
262
+ break
263
+ frames = self.frame_buffer.pop_all()
264
+ if frames:
265
+ for frame in frames:
266
+ await self._handle_frame(frame)
267
+ continue
268
+ data = await asyncio.wait_for(self.reader.read(65535), timeout=self.config.http.read_timeout)
269
+ if not data:
270
+ break
271
+ self.frame_buffer.feed(data)
272
+ finally:
273
+ if self.keepalive_task is not None:
274
+ self.keepalive_task.cancel()
275
+ with suppress(asyncio.CancelledError):
276
+ await self.keepalive_task
277
+ await self._shutdown_streams()
278
+
279
+ async def _ensure_preface(self) -> None:
280
+ if self.prebuffer == H2_PREFACE:
281
+ self.state.preface_seen = True
282
+ return
283
+ if self.prebuffer:
284
+ raise ProtocolError("unexpected HTTP/2 prebuffer state")
285
+ received = await self.reader.readexactly(len(H2_PREFACE))
286
+ if received != H2_PREFACE:
287
+ raise ProtocolError("invalid HTTP/2 client preface")
288
+ self.state.preface_seen = True
289
+
290
+ def _check_frame_header(self, frame: HTTP2Frame) -> None:
291
+ if frame.length > self.state.local_settings[0x5]:
292
+ raise ProtocolError("received HTTP/2 frame exceeds local MAX_FRAME_SIZE")
293
+ if not self.state.remote_settings_seen and frame.frame_type != FRAME_SETTINGS:
294
+ raise ProtocolError("HTTP/2 first frame after preface must be SETTINGS")
295
+ if self._continuation_stream_id is not None and (
296
+ frame.frame_type != FRAME_CONTINUATION or frame.stream_id != self._continuation_stream_id
297
+ ):
298
+ raise ProtocolError("unexpected frame while CONTINUATION is pending")
299
+
300
+ async def _handle_frame(self, frame: HTTP2Frame) -> None:
301
+ self._check_frame_header(frame)
302
+ self._record_keepalive_activity()
303
+ if frame.frame_type == FRAME_SETTINGS:
304
+ await self._handle_settings(frame)
305
+ return
306
+ if frame.frame_type == FRAME_HEADERS:
307
+ await self._handle_headers(frame)
308
+ return
309
+ if frame.frame_type == FRAME_CONTINUATION:
310
+ await self._handle_continuation(frame)
311
+ return
312
+ if frame.frame_type == FRAME_DATA:
313
+ await self._handle_data(frame)
314
+ return
315
+ if frame.frame_type == FRAME_WINDOW_UPDATE:
316
+ await self._handle_window_update(frame)
317
+ return
318
+ if frame.frame_type == FRAME_PING:
319
+ await self._handle_ping(frame)
320
+ return
321
+ if frame.frame_type == FRAME_PRIORITY:
322
+ self._handle_priority(frame)
323
+ return
324
+ if frame.frame_type == FRAME_PUSH_PROMISE:
325
+ self._handle_push_promise(frame)
326
+ return
327
+ if frame.frame_type == FRAME_RST_STREAM:
328
+ await self._handle_rst_stream(frame)
329
+ return
330
+ if frame.frame_type == FRAME_GOAWAY:
331
+ self._handle_goaway(frame)
332
+ return
333
+ # Unknown extension frames are ignored unless a CONTINUATION sequence is pending.
334
+
335
+ async def _handle_settings(self, frame: HTTP2Frame) -> None:
336
+ if frame.stream_id != 0:
337
+ raise ProtocolError("SETTINGS must use stream 0")
338
+ if frame.flags & FLAG_ACK:
339
+ if not self.state.remote_settings_seen:
340
+ raise ProtocolError("HTTP/2 peer must send initial SETTINGS before ACK")
341
+ if frame.payload:
342
+ raise ProtocolError("ACK SETTINGS must have empty payload")
343
+ return
344
+ self.state.remote_settings_seen = True
345
+ settings = decode_settings(frame.payload)
346
+ if 0x1 in settings:
347
+ self.hpack_encoder.set_max_table_size(settings[0x1])
348
+ old_initial_window = self.state.remote_settings.get(0x4, DEFAULT_SETTINGS[0x4])
349
+ self.state.remote_settings.update(settings)
350
+ new_initial_window = self.state.remote_settings.get(0x4, DEFAULT_SETTINGS[0x4])
351
+ delta = new_initial_window - old_initial_window
352
+ if delta:
353
+ self.streams.apply_window_delta(delta)
354
+ if delta > 0:
355
+ self._notify_waiter(0)
356
+ self.frame_writer.max_frame_size = self.state.max_frame_size
357
+ await self._write_raw(serialize_settings_ack())
358
+
359
+ def _validate_new_remote_stream(self, stream_id: int) -> None:
360
+ if stream_id % 2 == 0:
361
+ raise ProtocolError("client-initiated HTTP/2 streams must use odd stream ids")
362
+ if stream_id <= self.state.highest_remote_stream_id:
363
+ raise ProtocolError("HTTP/2 stream ids must increase")
364
+ if self.state.peer_goaway_received or self.state.local_goaway_sent:
365
+ raise ProtocolError("HTTP/2 new stream received after GOAWAY")
366
+ if self.streams.active_remote_stream_count() >= self.state.max_concurrent_streams:
367
+ raise ProtocolError("HTTP/2 maximum concurrent streams exceeded")
368
+ self.state.highest_remote_stream_id = stream_id
369
+ self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
370
+
371
+ def _append_header_fragment(self, state: H2StreamState, fragment: bytes) -> None:
372
+ next_size = state.header_block_bytes + len(fragment)
373
+ if next_size > self.config.http.http2_max_headers_size:
374
+ raise ProtocolError("request head exceeds configured http2_max_headers_size")
375
+ state.header_block_bytes = next_size
376
+ state.header_fragments.append(fragment)
377
+
378
+ def _validate_header_list_size(self, headers: list[tuple[bytes, bytes]]) -> None:
379
+ size = sum(len(name) + len(value) + 32 for name, value in headers)
380
+ if size > self.state.max_header_list_size:
381
+ raise ProtocolError("HTTP/2 header list exceeds configured maximum")
382
+
383
+ def _validate_trailer_headers(self, headers: list[tuple[bytes, bytes]]) -> None:
384
+ for name, value in headers:
385
+ if any(65 <= byte <= 90 for byte in name):
386
+ raise ProtocolError("uppercase header field name forbidden in HTTP/2")
387
+ if name.startswith(b":"):
388
+ raise ProtocolError("trailer pseudo-header forbidden in HTTP/2")
389
+ if name in {b"connection", b"upgrade", b"proxy-connection", b"transfer-encoding"}:
390
+ raise ProtocolError("connection-specific header forbidden in HTTP/2")
391
+ if name == b"te" and value.lower() != b"trailers":
392
+ raise ProtocolError("invalid TE header for HTTP/2")
393
+
394
+ async def _handle_headers(self, frame: HTTP2Frame) -> None:
395
+ if frame.stream_id == 0:
396
+ raise ProtocolError("HEADERS must use a stream id")
397
+ if self._continuation_stream_id not in (None, frame.stream_id):
398
+ raise ProtocolError("unexpected HEADERS while CONTINUATION is pending")
399
+ state = self.streams.find(frame.stream_id)
400
+ is_new_stream = state is None
401
+ if is_new_stream:
402
+ if self.streams.is_closed(frame.stream_id):
403
+ raise ProtocolError("HEADERS on closed HTTP/2 stream")
404
+ self._validate_new_remote_stream(frame.stream_id)
405
+ state = self.streams.activate_remote(
406
+ frame.stream_id,
407
+ send_window=self.state.initial_window_size,
408
+ receive_window=self.state.local_initial_window_size,
409
+ )
410
+ state.current_header_block_is_trailers = False
411
+ state.open_remote(end_stream=bool(frame.flags & FLAG_END_STREAM))
412
+ else:
413
+ if state.closed:
414
+ raise ProtocolError("HEADERS on closed HTTP/2 stream")
415
+ if not state.headers_complete:
416
+ raise ProtocolError("duplicate HTTP/2 initial HEADERS block")
417
+ if state.awaiting_continuation:
418
+ raise ProtocolError("unexpected HEADERS while CONTINUATION is pending")
419
+ if state.lifecycle not in {H2StreamLifecycle.OPEN, H2StreamLifecycle.HALF_CLOSED_LOCAL}:
420
+ raise ProtocolError("HEADERS not permitted in current HTTP/2 stream state")
421
+ if state.end_stream_received or state.trailers_complete:
422
+ raise ProtocolError("trailing HEADERS not permitted after end of stream")
423
+ if not (frame.flags & FLAG_END_STREAM):
424
+ raise ProtocolError("trailing HTTP/2 HEADERS must carry END_STREAM")
425
+ state.current_header_block_is_trailers = True
426
+ state.receive_end_stream()
427
+ self._append_header_fragment(state, headers_payload_fragment(frame.payload, frame.flags))
428
+ state.awaiting_continuation = not bool(frame.flags & FLAG_END_HEADERS)
429
+ if state.awaiting_continuation:
430
+ self._continuation_stream_id = frame.stream_id
431
+ return
432
+ self._continuation_stream_id = None
433
+ self._finish_headers(state)
434
+ await self._maybe_dispatch(frame.stream_id)
435
+
436
+ async def _handle_continuation(self, frame: HTTP2Frame) -> None:
437
+ if frame.stream_id == 0:
438
+ raise ProtocolError("CONTINUATION must use a stream id")
439
+ if self._continuation_stream_id != frame.stream_id:
440
+ raise ProtocolError("unexpected CONTINUATION stream")
441
+ state = self.streams.find(frame.stream_id)
442
+ if state is None:
443
+ raise ProtocolError("CONTINUATION for unknown stream")
444
+ self._append_header_fragment(state, frame.payload)
445
+ state.awaiting_continuation = not bool(frame.flags & FLAG_END_HEADERS)
446
+ if state.awaiting_continuation:
447
+ return
448
+ self._continuation_stream_id = None
449
+ self._finish_headers(state)
450
+ await self._maybe_dispatch(frame.stream_id)
451
+
452
+ async def _consume_receive_flow(self, stream_id: int, amount: int) -> None:
453
+ if amount <= 0:
454
+ return
455
+ self.state.connection_receive_window.consume(amount)
456
+ if self.state.connection_receive_window.available < 0:
457
+ raise ProtocolError("HTTP/2 connection flow-control window exceeded")
458
+ state = self.streams.find(stream_id)
459
+ if state is None:
460
+ raise ProtocolError("HTTP/2 stream flow-control used after closure")
461
+ state.receive_window.consume(amount)
462
+ if state.receive_window.available < 0:
463
+ raise ProtocolError("HTTP/2 stream flow-control window exceeded")
464
+
465
+ async def _maybe_replenish_receive_credit(self, stream_id: int, amount: int) -> None:
466
+ if amount <= 0:
467
+ return
468
+ updates: list[bytes] = []
469
+ self.state.connection_receive_consumed_since_update += amount
470
+ connection_increment = 0
471
+ if self.config.http.http2_adaptive_window:
472
+ new_connection_target = next_adaptive_window_target(
473
+ self.state.connection_receive_window_target,
474
+ max(amount, self.state.connection_receive_consumed_since_update),
475
+ )
476
+ if new_connection_target > self.state.connection_receive_window_target:
477
+ delta_target = new_connection_target - self.state.connection_receive_window_target
478
+ self.state.connection_receive_window_target = new_connection_target
479
+ self.state.connection_receive_window.increase(delta_target)
480
+ connection_increment += delta_target
481
+ connection_threshold = max(1, self.state.connection_receive_window_target // 2)
482
+ if (
483
+ self.state.connection_receive_window.available <= connection_threshold
484
+ or self.state.connection_receive_consumed_since_update >= connection_threshold
485
+ ):
486
+ increment = self.state.connection_receive_consumed_since_update
487
+ self.state.connection_receive_consumed_since_update = 0
488
+ self.state.connection_receive_window.increase(increment)
489
+ connection_increment += increment
490
+ if connection_increment > 0:
491
+ updates.append(serialize_window_update(0, connection_increment))
492
+ state = self.streams.find(stream_id)
493
+ if state is None:
494
+ for update in updates:
495
+ await self._write_raw(update)
496
+ return
497
+ state.receive_consumed_since_update += amount
498
+ stream_increment = 0
499
+ if self.config.http.http2_adaptive_window:
500
+ new_stream_target = next_adaptive_window_target(
501
+ state.receive_window_target,
502
+ max(amount, state.receive_consumed_since_update),
503
+ )
504
+ if new_stream_target > state.receive_window_target:
505
+ delta_target = new_stream_target - state.receive_window_target
506
+ state.receive_window_target = new_stream_target
507
+ state.receive_window.increase(delta_target)
508
+ stream_increment += delta_target
509
+ stream_threshold = max(1, state.receive_window_target // 2)
510
+ if state.receive_window.available <= stream_threshold or state.receive_consumed_since_update >= stream_threshold:
511
+ increment = state.receive_consumed_since_update
512
+ state.receive_consumed_since_update = 0
513
+ state.receive_window.increase(increment)
514
+ stream_increment += increment
515
+ if stream_increment > 0:
516
+ updates.append(serialize_window_update(stream_id, stream_increment))
517
+ for update in updates:
518
+ await self._write_raw(update)
519
+
520
+ async def _handle_data(self, frame: HTTP2Frame) -> None:
521
+ if frame.stream_id == 0:
522
+ raise ProtocolError("DATA must use a stream id")
523
+ if self.streams.is_closed(frame.stream_id):
524
+ return
525
+ state = self.streams.find(frame.stream_id)
526
+ if state is None:
527
+ raise ProtocolError("DATA on idle HTTP/2 stream")
528
+ if state.awaiting_continuation:
529
+ raise ProtocolError("DATA received before END_HEADERS")
530
+ if not state.headers_complete:
531
+ raise ProtocolError("DATA before HEADERS")
532
+ if state.trailers_complete or state.end_stream_received or state.closed:
533
+ raise ProtocolError("DATA on half-closed HTTP/2 stream")
534
+ payload = strip_padding(frame.payload, frame.flags)
535
+ await self._consume_receive_flow(frame.stream_id, len(payload))
536
+ if state.websocket_session is not None:
537
+ await state.websocket_session.feed_data(payload, end_stream=bool(frame.flags & FLAG_END_STREAM))
538
+ elif state.connect_tunnel is not None:
539
+ await state.connect_tunnel.feed_client_data(payload, end_stream=bool(frame.flags & FLAG_END_STREAM))
540
+ elif payload:
541
+ if state.buffered_body_size + len(payload) > self.config.max_body_size:
542
+ raise ProtocolError("request body exceeds configured max_body_size")
543
+ state.append_body(payload)
544
+ await self._maybe_replenish_receive_credit(frame.stream_id, len(payload))
545
+ if frame.flags & FLAG_END_STREAM:
546
+ state.receive_end_stream()
547
+ await self._maybe_dispatch(frame.stream_id)
548
+ self._finalize_stream_if_complete(frame.stream_id)
549
+
550
+ def _finish_headers(self, state: H2StreamState) -> None:
551
+ block = b"".join(state.header_fragments)
552
+ headers = self.hpack_decoder.decode_header_block(block)
553
+ self._validate_header_list_size(headers)
554
+ if state.current_header_block_is_trailers:
555
+ self._validate_trailer_headers(headers)
556
+ state.trailers = headers
557
+ state.trailers_complete = True
558
+ else:
559
+ state.headers = headers
560
+ state.headers_complete = True
561
+ state.header_fragments.clear()
562
+ state.header_block_bytes = 0
563
+ state.awaiting_continuation = False
564
+ state.current_header_block_is_trailers = False
565
+
566
+ async def _maybe_dispatch(self, stream_id: int) -> None:
567
+ state = self.streams.find(stream_id)
568
+ if state is None or state.dispatched or not state.headers_complete:
569
+ return
570
+ is_ws = self._is_extended_connect_websocket(state.headers)
571
+ is_connect = self._is_generic_connect_tunnel(state.headers)
572
+ if not is_ws and not is_connect and not state.end_stream_received:
573
+ return
574
+ if not self._admit_stream_work(stream_id):
575
+ request = self._build_request(state)
576
+ await self._send_response(stream_id, 503, [(b"content-type", b"text/plain")], b"scheduler overloaded")
577
+ self.access_logger.log_http(self.client, request.method, request.path, 503, "HTTP/2")
578
+ self._release_stream_work_lease(stream_id)
579
+ self._cancel_stream(stream_id)
580
+ self.streams.close(stream_id)
581
+ self._maybe_finish_after_goaway()
582
+ return
583
+ state.dispatched = True
584
+ if is_ws:
585
+ await self._start_websocket_stream(stream_id)
586
+ return
587
+ if is_connect:
588
+ await self._start_connect_tunnel(stream_id)
589
+ return
590
+ self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
591
+ task = asyncio.create_task(self._run_stream(stream_id), name=f"tigrcorn-h2-stream-{stream_id}")
592
+ self.stream_tasks[stream_id] = task
593
+
594
+ async def _handle_window_update(self, frame: HTTP2Frame) -> None:
595
+ increment = parse_window_update(frame.payload)
596
+ if frame.stream_id == 0:
597
+ self.state.connection_send_window.increase(increment)
598
+ self._notify_waiter(0)
599
+ return
600
+ if self.streams.is_closed(frame.stream_id):
601
+ return
602
+ state = self.streams.find(frame.stream_id)
603
+ if state is None:
604
+ raise ProtocolError("WINDOW_UPDATE on idle HTTP/2 stream")
605
+ state.send_window.increase(increment)
606
+ self._notify_waiter(frame.stream_id)
607
+
608
+ async def _handle_ping(self, frame: HTTP2Frame) -> None:
609
+ if frame.stream_id != 0:
610
+ raise ProtocolError("PING must use stream 0")
611
+ if len(frame.payload) != 8:
612
+ raise ProtocolError("PING payload must be 8 bytes")
613
+ if frame.flags & FLAG_ACK:
614
+ if self.keepalive is not None:
615
+ self.keepalive.acknowledge_pong(frame.payload)
616
+ return
617
+ await self._write_raw(serialize_ping(frame.payload, ack=True))
618
+
619
+ def _handle_priority(self, frame: HTTP2Frame) -> None:
620
+ if frame.stream_id == 0:
621
+ raise ProtocolError("PRIORITY must use a stream id")
622
+ _exclusive, dependency, _weight = parse_priority(frame.payload)
623
+ if dependency == frame.stream_id:
624
+ raise ProtocolError("HTTP/2 PRIORITY stream dependency cannot depend on itself")
625
+
626
+ def _handle_push_promise(self, frame: HTTP2Frame) -> None:
627
+ if frame.stream_id == 0:
628
+ raise ProtocolError("PUSH_PROMISE must use a stream id")
629
+ raise ProtocolError("clients must not send PUSH_PROMISE to an HTTP/2 server")
630
+
631
+ async def _handle_rst_stream(self, frame: HTTP2Frame) -> None:
632
+ if frame.stream_id == 0 or len(frame.payload) != 4:
633
+ raise ProtocolError("invalid RST_STREAM frame")
634
+ if self.streams.is_closed(frame.stream_id):
635
+ return
636
+ state = self.streams.find(frame.stream_id)
637
+ if state is None or (not state.opened and not state.reserved_local and not state.reserved_remote):
638
+ raise ProtocolError("RST_STREAM on idle HTTP/2 stream")
639
+ if state.websocket_session is not None:
640
+ await state.websocket_session.abort()
641
+ if state.connect_tunnel is not None:
642
+ await state.connect_tunnel.abort()
643
+ self._cancel_stream(frame.stream_id)
644
+ state.mark_reset_received()
645
+ self.streams.close(frame.stream_id)
646
+ self._notify_waiter(frame.stream_id)
647
+ self._maybe_finish_after_goaway()
648
+
649
+ def _handle_goaway(self, frame: HTTP2Frame) -> None:
650
+ if frame.stream_id != 0:
651
+ raise ProtocolError("GOAWAY must use stream 0")
652
+ last_stream_id, _error_code, _debug_data = parse_goaway(frame.payload)
653
+ if self.state.peer_goaway_received and self.state.peer_last_stream_id is not None:
654
+ if last_stream_id > self.state.peer_last_stream_id:
655
+ raise ProtocolError("HTTP/2 GOAWAY last_stream_id must not increase")
656
+ self.state.peer_goaway_received = True
657
+ self.state.peer_last_stream_id = last_stream_id
658
+ self.state.shutdown = True
659
+ self._maybe_finish_after_goaway()
660
+
661
+ def _should_finish_after_peer_goaway(self) -> bool:
662
+ return (
663
+ self.state.peer_goaway_received
664
+ and self._continuation_stream_id is None
665
+ and not self.streams.streams
666
+ and not self.stream_tasks
667
+ )
668
+
669
+ def _maybe_finish_after_goaway(self) -> None:
670
+ if self._should_finish_after_peer_goaway():
671
+ self.running = False
672
+
673
+ def _pseudo_headers(self, headers: list[tuple[bytes, bytes]]) -> dict[bytes, bytes]:
674
+ return {k: v for k, v in headers if k.startswith(b":")}
675
+
676
+ def _is_extended_connect_websocket(self, headers: list[tuple[bytes, bytes]]) -> bool:
677
+ pseudo = self._pseudo_headers(headers)
678
+ return pseudo.get(b":method") == b"CONNECT" and pseudo.get(b":protocol") == b"websocket"
679
+
680
+ def _is_generic_connect_tunnel(self, headers: list[tuple[bytes, bytes]]) -> bool:
681
+ pseudo = self._pseudo_headers(headers)
682
+ return pseudo.get(b":method") == b"CONNECT" and pseudo.get(b":protocol") is None
683
+ def _release_stream_work_lease(self, stream_id: int) -> None:
684
+ lease = self.stream_work_leases.pop(stream_id, None)
685
+ if lease is not None:
686
+ lease.release()
687
+
688
+ def _on_websocket_stream_closed(self, stream_id: int) -> None:
689
+ state = self.streams.find(stream_id)
690
+ if state is not None:
691
+ state.websocket_session = None
692
+ self._release_stream_work_lease(stream_id)
693
+ self._finalize_stream_if_complete(stream_id)
694
+
695
+ def _admit_stream_work(self, stream_id: int) -> bool:
696
+ if self.scheduler is None:
697
+ return True
698
+ lease = self.scheduler.acquire_work()
699
+ if lease is None:
700
+ if self.metrics is not None:
701
+ self.metrics.scheduler_task_rejected()
702
+ return False
703
+ self.stream_work_leases[stream_id] = lease
704
+ return True
705
+
706
+
707
+ def _next_local_push_stream_id(self) -> int:
708
+ max_local_streams = self.state.remote_settings.get(0x3)
709
+ if max_local_streams is not None and self.streams.active_local_stream_count() >= max_local_streams:
710
+ raise ProtocolError("HTTP/2 peer refused additional server-initiated streams")
711
+ stream_id = self.state.next_local_stream_id
712
+ while self.streams.find(stream_id) is not None or self.streams.is_closed(stream_id):
713
+ stream_id += 2
714
+ if stream_id > 0x7FFFFFFF:
715
+ raise ProtocolError("exhausted HTTP/2 server-initiated stream identifiers")
716
+ self.state.next_local_stream_id = stream_id + 2
717
+ return stream_id
718
+
719
+ def _build_push_request(self, parent_stream_id: int, message: dict) -> ParsedRequest:
720
+ state = self.streams.find(parent_stream_id)
721
+ if state is None:
722
+ raise ProtocolError("cannot create HTTP/2 server push from an unknown stream")
723
+ if self._is_extended_connect_websocket(state.headers) or self._is_generic_connect_tunnel(state.headers):
724
+ raise ProtocolError("HTTP/2 server push is not available on CONNECT streams")
725
+ pseudo = self._pseudo_headers(state.headers)
726
+ path = message.get("path")
727
+ if not path:
728
+ raise ProtocolError("http.response.push requires a path")
729
+ if isinstance(path, bytes):
730
+ target = path.decode("ascii", "strict")
731
+ else:
732
+ target = str(path)
733
+ method = message.get("method", "GET")
734
+ if isinstance(method, bytes):
735
+ method_text = method.decode("ascii", "strict").upper()
736
+ else:
737
+ method_text = str(method).upper()
738
+ authority = message.get("authority")
739
+ if authority is None:
740
+ authority_bytes = pseudo.get(b":authority", b"")
741
+ elif isinstance(authority, bytes):
742
+ authority_bytes = authority
743
+ else:
744
+ authority_bytes = str(authority).encode("ascii", "strict")
745
+ scheme = message.get("scheme")
746
+ if scheme is None:
747
+ scheme_bytes = pseudo.get(b":scheme", self.scheme.encode("ascii"))
748
+ elif isinstance(scheme, bytes):
749
+ scheme_bytes = scheme
750
+ else:
751
+ scheme_bytes = str(scheme).encode("ascii", "strict")
752
+ extra_headers = [
753
+ (bytes(name).lower(), bytes(value))
754
+ for name, value in message.get("headers", [])
755
+ if not bytes(name).startswith(b":")
756
+ ]
757
+ split = urlsplit(target)
758
+ path_text = split.path or "/"
759
+ raw_path = path_text.encode("utf-8")
760
+ query_string = split.query.encode("ascii")
761
+ pseudo_headers = [
762
+ (b":method", method_text.encode("ascii")),
763
+ (b":path", target.encode("utf-8")),
764
+ (b":scheme", scheme_bytes),
765
+ (b":authority", authority_bytes),
766
+ ]
767
+ return ParsedRequest(
768
+ method=method_text,
769
+ target=target,
770
+ path=path_text,
771
+ raw_path=raw_path,
772
+ query_string=query_string,
773
+ http_version="2",
774
+ headers=extra_headers,
775
+ body=b"",
776
+ keep_alive=True,
777
+ expect_continue=False,
778
+ websocket_upgrade=False,
779
+ ), pseudo_headers + extra_headers
780
+
781
+ async def _run_http_app(self, stream_id: int, request: ParsedRequest, *, allow_push: bool) -> tuple[int, list[tuple[bytes, bytes]], bytes, list[tuple[bytes, bytes]], list[tuple[int, list[tuple[bytes, bytes]]]], list | None, object | None]:
782
+ extensions = dict(self.scope_extensions)
783
+ state = self.streams.find(stream_id)
784
+ raw_request_trailers = list(state.trailers) if state is not None else []
785
+ try:
786
+ request_trailers = apply_request_trailer_policy(raw_request_trailers, self.config.http.trailer_policy)
787
+ except ProtocolError:
788
+ return 400, [(b"content-type", b"text/plain")], b"bad request trailers", [], [], None, None
789
+ if request.method.upper() == "CONNECT":
790
+ extensions["tigrcorn.http.connect"] = {"authority": request.target}
791
+ if request_trailers and self.config.http.trailer_policy != 'drop':
792
+ extensions["tigrcorn.http.request_trailers"] = {}
793
+ if allow_push and self.state.client_allows_push:
794
+ extensions["http.response.push"] = {}
795
+ extensions['tigrcorn.http.response.file'] = {'protocol': 'http/2', 'streaming': True, 'sendfile': False}
796
+ extensions['http.response.pathsend'] = {}
797
+ scope = build_http_scope(request, client=self.client, server=self.server, scheme=self.scheme, extensions=extensions, root_path=self.config.proxy.root_path, proxy=self.config.proxy)
798
+ receive = HTTPRequestReceive(request.body, trailers=request_trailers, trailer_policy=self.config.http.trailer_policy)
799
+ collector = HTTPResponseCollector()
800
+
801
+ async def send(message: dict) -> None:
802
+ if message.get("type") == "http.response.push":
803
+ if not allow_push or not self.state.client_allows_push:
804
+ raise ProtocolError("HTTP/2 server push is not available on this stream")
805
+ await self._send_push_promise(stream_id, message)
806
+ return
807
+ await collector(message)
808
+
809
+ status = 500
810
+ cleanup: object | None = None
811
+ try:
812
+ await self.app(scope, receive, send)
813
+ collector.finalize()
814
+ assert collector.status is not None
815
+ status = collector.status
816
+ headers = list(collector.headers)
817
+ trailers = list(collector.trailers)
818
+ informational = list(collector.informational_responses)
819
+ body_segments = list(collector.body_segments) if collector.uses_streamed_body else None
820
+ if body_segments is not None:
821
+ cleanup = collector.cleanup if collector.has_spooled_body() else None
822
+ return status, headers, b'', trailers, informational, body_segments, cleanup
823
+ if collector.has_spooled_body():
824
+ spooled_segments = collector.spooled_body_segments()
825
+ spooled_path = ''
826
+ if spooled_segments:
827
+ first_segment = spooled_segments[0]
828
+ spooled_path = getattr(first_segment, 'path', '')
829
+ planned = plan_file_backed_response_entity_semantics(
830
+ method=request.method,
831
+ request_headers=request.headers,
832
+ response_headers=headers,
833
+ status=status,
834
+ body_path=spooled_path,
835
+ body_length=collector.body_length,
836
+ generated_etag=collector.generated_entity_tag(),
837
+ apply_content_coding=True,
838
+ trailers_present=bool(trailers) and request.method.upper() != 'HEAD',
839
+ )
840
+ cleanup = collector.cleanup
841
+ if planned.requires_materialization:
842
+ body = await collector.materialize_body()
843
+ processed = apply_response_entity_semantics(
844
+ method=request.method,
845
+ request_headers=request.headers,
846
+ response_headers=headers,
847
+ body=body,
848
+ status=status,
849
+ content_coding_policy=self.config.http.content_coding_policy,
850
+ supported_codings=tuple(self.config.http.content_codings),
851
+ apply_content_coding=True,
852
+ generate_etag=True,
853
+ )
854
+ return processed.status, processed.headers, processed.body, ([] if processed.head_response else trailers), informational, None, cleanup
855
+ if planned.use_body_segments:
856
+ return planned.status, planned.headers, b'', trailers, informational, list(planned.body_segments), cleanup
857
+ return planned.status, planned.headers, planned.body, [], informational, None, cleanup
858
+ body = await collector.materialize_body()
859
+ except Exception:
860
+ collector.cleanup()
861
+ status, headers, body, trailers = 500, [(b"content-type", b"text/plain")], b"internal server error", []
862
+ informational = []
863
+ body_segments = None
864
+ cleanup = None
865
+ processed = apply_response_entity_semantics(
866
+ method=request.method,
867
+ request_headers=request.headers,
868
+ response_headers=headers,
869
+ body=body,
870
+ status=status,
871
+ content_coding_policy=self.config.http.content_coding_policy,
872
+ supported_codings=tuple(self.config.http.content_codings),
873
+ apply_content_coding=True,
874
+ generate_etag=True,
875
+ )
876
+ return processed.status, processed.headers, processed.body, ([] if processed.head_response else trailers), informational, None, cleanup
877
+
878
+ async def _send_push_promise(self, parent_stream_id: int, message: dict) -> None:
879
+ if not self.state.client_allows_push:
880
+ return
881
+ promised_stream_id = self._next_local_push_stream_id()
882
+ request, request_headers = self._build_push_request(parent_stream_id, message)
883
+ header_block = self.hpack_encoder.encode_header_block(request_headers)
884
+ await self._write_raw(self.frame_writer.push_promise(parent_stream_id, promised_stream_id, header_block))
885
+ self.streams.reserve_local(
886
+ promised_stream_id,
887
+ send_window=self.state.initial_window_size,
888
+ receive_window=self.state.local_initial_window_size,
889
+ )
890
+ self.state.last_stream_id = max(self.state.last_stream_id, promised_stream_id)
891
+ status, headers, body, trailers, informational, body_segments, cleanup = await self._run_http_app(promised_stream_id, request, allow_push=False)
892
+ for interim_status, interim_headers in informational:
893
+ await self._send_stream_headers(promised_stream_id, interim_status, sanitize_early_hints_headers(interim_headers), end_stream=False)
894
+ try:
895
+ await self._send_response(promised_stream_id, status, headers, body, trailers, body_segments=body_segments)
896
+ finally:
897
+ if cleanup is not None:
898
+ cleanup()
899
+ if self.streams.find(promised_stream_id) is not None:
900
+ self._cancel_stream(promised_stream_id)
901
+ self.streams.close(promised_stream_id)
902
+
903
+ def _finalize_stream_if_complete(self, stream_id: int) -> None:
904
+ state = self.streams.find(stream_id)
905
+ if state is None or state.websocket_session is not None or state.connect_tunnel is not None:
906
+ return
907
+ if state.local_closed and state.end_stream_received:
908
+ self._release_stream_work_lease(stream_id)
909
+ self._cancel_stream(stream_id)
910
+ self.streams.close(stream_id)
911
+ self._maybe_finish_after_goaway()
912
+
913
+ async def _reset_connect_stream(self, stream_id: int) -> None:
914
+ state = self.streams.find(stream_id)
915
+ if state is None or state.closed:
916
+ return
917
+ if not state.reset_sent:
918
+ with suppress(Exception):
919
+ await self._write_raw(serialize_rst_stream(stream_id, H2_CONNECT_ERROR))
920
+ state.mark_reset_sent()
921
+ self._cancel_stream(stream_id)
922
+ self.streams.close(stream_id)
923
+ self._maybe_finish_after_goaway()
924
+
925
+ async def _send_stream_data(self, stream_id: int, data: bytes, *, end_stream: bool = False) -> None:
926
+ state = self.streams.find(stream_id)
927
+ if state is None or state.closed:
928
+ raise ProtocolError("attempted to send DATA on a closed HTTP/2 stream")
929
+ if not data and not end_stream:
930
+ return
931
+ if not data:
932
+ await self._write_raw(self.frame_writer.data(stream_id, b"", end_stream=True))
933
+ state.send_end_stream()
934
+ return
935
+ offset = 0
936
+ while offset < len(data):
937
+ chunk_size = min(self.state.max_frame_size, len(data) - offset)
938
+ while self.state.connection_send_window.available <= 0 or state.send_window.available <= 0:
939
+ await self._wait_for_credit(stream_id)
940
+ allowed = min(chunk_size, self.state.connection_send_window.available, state.send_window.available)
941
+ if allowed <= 0:
942
+ await self._wait_for_credit(stream_id)
943
+ continue
944
+ chunk = data[offset : offset + allowed]
945
+ offset += len(chunk)
946
+ self.state.connection_send_window.consume(len(chunk))
947
+ state.send_window.consume(len(chunk))
948
+ final_chunk = end_stream and offset == len(data)
949
+ await self._write_raw(self.frame_writer.data(stream_id, chunk, end_stream=final_chunk))
950
+ if final_chunk:
951
+ state.send_end_stream()
952
+
953
+ async def _send_stream_headers(
954
+ self,
955
+ stream_id: int,
956
+ status: int,
957
+ headers: list[tuple[bytes, bytes]],
958
+ end_stream: bool,
959
+ ) -> None:
960
+ state = self.streams.find(stream_id)
961
+ if state is None or state.closed:
962
+ raise ProtocolError("attempted to send HEADERS on a closed HTTP/2 stream")
963
+ normalized_headers = sanitize_early_hints_headers(headers) if status == 103 else strip_connection_specific_headers(headers)
964
+ policy_headers = apply_response_header_policy(
965
+ normalized_headers,
966
+ server_header=self.config.server_header_value,
967
+ include_date_header=self.config.include_date_header,
968
+ default_headers=self.config.default_response_headers,
969
+ alt_svc_values=() if status < 200 else configured_alt_svc_values(self.config, request_http_version='2'),
970
+ )
971
+ header_block = self.hpack_encoder.encode_header_block([(b":status", str(status).encode("ascii")), *policy_headers])
972
+ await self._write_raw(self.frame_writer.headers(stream_id, header_block, end_stream=end_stream))
973
+ if end_stream:
974
+ state.send_end_stream()
975
+
976
+ async def _start_connect_tunnel(self, stream_id: int) -> None:
977
+ state = self.streams.find(stream_id)
978
+ if state is None:
979
+ raise ProtocolError("connect stream disappeared before dispatch")
980
+ request = self._build_request(state)
981
+ try:
982
+ host, port = parse_connect_authority(request.target)
983
+ except Exception:
984
+ await self._send_response(stream_id, 400, [(b"content-type", b"text/plain")], b"bad connect target")
985
+ self.access_logger.log_http(self.client, "CONNECT", request.target, 400, "HTTP/2")
986
+ self._release_stream_work_lease(stream_id)
987
+ self._cancel_stream(stream_id)
988
+ self.streams.close(stream_id)
989
+ self._maybe_finish_after_goaway()
990
+ return
991
+ if self.config.http.connect_policy == 'deny':
992
+ await self._send_response(stream_id, 403, [(b"content-type", b"text/plain")], b"connect denied")
993
+ self.access_logger.log_http(self.client, "CONNECT", request.target, 403, "HTTP/2")
994
+ self._cancel_stream(stream_id)
995
+ self.streams.close(stream_id)
996
+ self._maybe_finish_after_goaway()
997
+ return
998
+ if self.config.http.connect_policy == 'allowlist' and not is_connect_allowed(host, port, self.config.http.connect_allow):
999
+ await self._send_response(stream_id, 403, [(b"content-type", b"text/plain")], b"connect denied")
1000
+ self.access_logger.log_http(self.client, "CONNECT", request.target, 403, "HTTP/2")
1001
+ self._cancel_stream(stream_id)
1002
+ self.streams.close(stream_id)
1003
+ self._maybe_finish_after_goaway()
1004
+ return
1005
+ try:
1006
+ upstream_reader, upstream_writer = await asyncio.wait_for(
1007
+ asyncio.open_connection(host, port),
1008
+ timeout=getattr(self.config, "read_timeout", 5.0),
1009
+ )
1010
+ except Exception:
1011
+ await self._send_response(stream_id, 502, [(b"content-type", b"text/plain")], b"bad gateway")
1012
+ self.access_logger.log_http(self.client, "CONNECT", request.target, 502, "HTTP/2")
1013
+ self._release_stream_work_lease(stream_id)
1014
+ self._cancel_stream(stream_id)
1015
+ self.streams.close(stream_id)
1016
+ self._maybe_finish_after_goaway()
1017
+ return
1018
+ tunnel = _HTTP2ConnectTunnel(
1019
+ handler=self,
1020
+ stream_id=stream_id,
1021
+ authority=request.target,
1022
+ upstream_reader=upstream_reader,
1023
+ upstream_writer=upstream_writer,
1024
+ work_lease=self.stream_work_leases.get(stream_id),
1025
+ )
1026
+ state.connect_tunnel = tunnel
1027
+ self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
1028
+ try:
1029
+ await tunnel.start()
1030
+ except Exception:
1031
+ state.connect_tunnel = None
1032
+ await close_tcp_writer(upstream_writer)
1033
+ raise
1034
+ if state.end_stream_received:
1035
+ await tunnel.feed_client_data(b'', end_stream=True)
1036
+ self.access_logger.log_http(self.client, "CONNECT", request.target, 200, "HTTP/2")
1037
+
1038
+ async def _send_h2_websocket_headers(
1039
+ self,
1040
+ stream_id: int,
1041
+ status: int,
1042
+ headers: list[tuple[bytes, bytes]],
1043
+ end_stream: bool,
1044
+ ) -> None:
1045
+ await self._send_stream_headers(stream_id, status, headers, end_stream)
1046
+
1047
+ async def _start_websocket_stream(self, stream_id: int) -> None:
1048
+ state = self.streams.find(stream_id)
1049
+ if state is None:
1050
+ raise ProtocolError("websocket stream disappeared before dispatch")
1051
+ request = self._build_request(state)
1052
+ authority = self._pseudo_headers(state.headers).get(b":authority")
1053
+ if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
1054
+ await self._send_response(stream_id, 421, [(b"content-type", b"text/plain")], b"misdirected request")
1055
+ self.access_logger.log_http(self.client, "CONNECT", request.path, 421, "HTTP/2")
1056
+ self._release_stream_work_lease(stream_id)
1057
+ self._cancel_stream(stream_id)
1058
+ self.streams.close(stream_id)
1059
+ self._maybe_finish_after_goaway()
1060
+ return
1061
+ session = H2WebSocketSession(
1062
+ app=self.app,
1063
+ config=self.config,
1064
+ request=request,
1065
+ client=self.client,
1066
+ server=self.server,
1067
+ scheme=self.scheme,
1068
+ send_headers=lambda status, headers, end_stream: self._send_stream_headers(stream_id, status, headers, end_stream),
1069
+ send_data=lambda data, end_stream: self._send_stream_data(stream_id, data, end_stream=end_stream),
1070
+ metrics=self.metrics,
1071
+ on_close=lambda stream_id=stream_id: self._on_websocket_stream_closed(stream_id),
1072
+ )
1073
+ state.websocket_session = session
1074
+ self.state.last_stream_id = max(self.state.last_stream_id, stream_id)
1075
+ await session.start()
1076
+
1077
+ def _validate_request_headers(self, headers: list[tuple[bytes, bytes]]) -> None:
1078
+ pseudo_seen: set[bytes] = set()
1079
+ regular_seen = False
1080
+ allowed_pseudo = {b":method", b":scheme", b":authority", b":path", b":protocol"}
1081
+ for name, value in headers:
1082
+ if any(65 <= byte <= 90 for byte in name):
1083
+ raise ProtocolError("uppercase header field name forbidden in HTTP/2")
1084
+ if name.startswith(b":"):
1085
+ if regular_seen:
1086
+ raise ProtocolError("pseudo-header after regular header")
1087
+ if name not in allowed_pseudo:
1088
+ raise ProtocolError("invalid request pseudo-header")
1089
+ if name in pseudo_seen:
1090
+ raise ProtocolError("duplicate pseudo-header")
1091
+ pseudo_seen.add(name)
1092
+ else:
1093
+ regular_seen = True
1094
+ if name in {b"connection", b"upgrade", b"proxy-connection", b"transfer-encoding"}:
1095
+ raise ProtocolError("connection-specific header forbidden in HTTP/2")
1096
+ if name == b"te" and value.lower() != b"trailers":
1097
+ raise ProtocolError("invalid TE header for HTTP/2")
1098
+ if b":method" not in pseudo_seen:
1099
+ raise ProtocolError("missing :method pseudo-header")
1100
+ method = dict(headers).get(b":method", b"GET")
1101
+ protocol = dict(headers).get(b":protocol")
1102
+ if protocol is not None:
1103
+ if method != b"CONNECT":
1104
+ raise ProtocolError("extended CONNECT requires CONNECT method")
1105
+ if self.state.local_settings.get(SETTING_ENABLE_CONNECT_PROTOCOL, 0) != 1:
1106
+ raise ProtocolError("extended CONNECT not enabled")
1107
+ if b":scheme" not in pseudo_seen or b":path" not in pseudo_seen or b":authority" not in pseudo_seen:
1108
+ raise ProtocolError("extended CONNECT missing required pseudo-headers")
1109
+ return
1110
+ if method == b"CONNECT":
1111
+ if b":authority" not in pseudo_seen:
1112
+ raise ProtocolError("CONNECT missing :authority pseudo-header")
1113
+ if b":scheme" in pseudo_seen or b":path" in pseudo_seen:
1114
+ raise ProtocolError("CONNECT must not include :scheme or :path pseudo-headers")
1115
+ return
1116
+ if b":scheme" not in pseudo_seen or b":path" not in pseudo_seen:
1117
+ raise ProtocolError("missing required request pseudo-header")
1118
+
1119
+ def _build_request(self, state: H2StreamState) -> ParsedRequest:
1120
+ self._validate_request_headers(state.headers)
1121
+ pseudo = {k: v for k, v in state.headers if k.startswith(b":")}
1122
+ headers = [(k, v) for k, v in state.headers if not k.startswith(b":")]
1123
+ method = pseudo.get(b":method", b"GET").decode("ascii", "strict")
1124
+ if method.upper() == "CONNECT" and pseudo.get(b":protocol") != b"websocket":
1125
+ target = pseudo.get(b":authority", b"").decode("ascii", "strict")
1126
+ path = target
1127
+ raw_path = target.encode("ascii", "strict")
1128
+ query_string = b""
1129
+ else:
1130
+ target = pseudo.get(b":path", b"/").decode("ascii", "strict")
1131
+ split = urlsplit(target)
1132
+ path = split.path or "/"
1133
+ raw_path = path.encode("utf-8")
1134
+ query_string = split.query.encode("ascii")
1135
+ return ParsedRequest(
1136
+ method=method,
1137
+ target=target,
1138
+ path=path,
1139
+ raw_path=raw_path,
1140
+ query_string=query_string,
1141
+ http_version="2",
1142
+ headers=headers,
1143
+ body=state.body,
1144
+ keep_alive=True,
1145
+ expect_continue=False,
1146
+ websocket_upgrade=False,
1147
+ )
1148
+
1149
+ async def _run_stream(self, stream_id: int) -> None:
1150
+ state = self.streams.find(stream_id)
1151
+ if state is None:
1152
+ self._release_stream_work_lease(stream_id)
1153
+ return
1154
+ request = self._build_request(state)
1155
+ authority = self._pseudo_headers(state.headers).get(b":authority")
1156
+ try:
1157
+ if self.config.allowed_server_names and not authority_allowed(authority, self.config.allowed_server_names):
1158
+ await self._send_response(stream_id, 421, [(b"content-type", b"text/plain")], b"misdirected request")
1159
+ self.access_logger.log_http(self.client, request.method, request.path, 421, "HTTP/2")
1160
+ if self.streams.find(stream_id) is not None:
1161
+ self._cancel_stream(stream_id)
1162
+ self.streams.close(stream_id)
1163
+ self._maybe_finish_after_goaway()
1164
+ return
1165
+ status, headers, body, trailers, informational, body_segments, cleanup = await self._run_http_app(stream_id, request, allow_push=True)
1166
+ for interim_status, interim_headers in informational:
1167
+ await self._send_stream_headers(stream_id, interim_status, sanitize_early_hints_headers(interim_headers), end_stream=False)
1168
+ try:
1169
+ await self._send_response(stream_id, status, headers, body, trailers, body_segments=body_segments)
1170
+ finally:
1171
+ if cleanup is not None:
1172
+ cleanup()
1173
+ self.access_logger.log_http(self.client, request.method, request.path, status, "HTTP/2")
1174
+ if self.streams.find(stream_id) is not None:
1175
+ self._cancel_stream(stream_id)
1176
+ self.streams.close(stream_id)
1177
+ self._maybe_finish_after_goaway()
1178
+ finally:
1179
+ self._release_stream_work_lease(stream_id)
1180
+
1181
+ async def _send_response(self, stream_id: int, status: int, headers: list[tuple[bytes, bytes]], body: bytes, trailers: list[tuple[bytes, bytes]] | None = None, *, body_segments: list | None = None) -> None:
1182
+ state = self.streams.find(stream_id)
1183
+ if state is None or state.closed:
1184
+ raise ProtocolError("attempted to send response on a closed HTTP/2 stream")
1185
+ streamed_body = response_body_segments_have_bytes(body_segments or []) if body_segments is not None else False
1186
+ if state.reserved_local and not state.opened:
1187
+ state.open_local_reserved(end_stream=not body and not streamed_body and not bool(trailers))
1188
+ headers = apply_response_header_policy(
1189
+ strip_connection_specific_headers(headers),
1190
+ server_header=self.config.server_header_value,
1191
+ include_date_header=self.config.include_date_header,
1192
+ default_headers=self.config.default_response_headers,
1193
+ alt_svc_values=configured_alt_svc_values(self.config, request_http_version='2'),
1194
+ )
1195
+ header_block = self.hpack_encoder.encode_header_block([(b":status", str(status).encode("ascii")), *headers])
1196
+ trailers = list(trailers or [])
1197
+ end_after_headers = not body and not streamed_body and not trailers
1198
+ await self._write_raw(self.frame_writer.headers(stream_id, header_block, end_stream=end_after_headers))
1199
+ if body_segments is not None:
1200
+ if not streamed_body and not trailers:
1201
+ state.send_end_stream()
1202
+ self._finalize_stream_if_complete(stream_id)
1203
+ return
1204
+ if streamed_body:
1205
+ async for chunk in iter_response_body_segments(body_segments, chunk_size=self.state.max_frame_size):
1206
+ await self._send_stream_data(stream_id, chunk, end_stream=False)
1207
+ if trailers:
1208
+ trailer_block = self.hpack_encoder.encode_header_block(trailers)
1209
+ await self._write_raw(self.frame_writer.headers(stream_id, trailer_block, end_stream=True))
1210
+ state.send_end_stream()
1211
+ self._finalize_stream_if_complete(stream_id)
1212
+ return
1213
+ await self._send_stream_data(stream_id, b'', end_stream=True)
1214
+ self._finalize_stream_if_complete(stream_id)
1215
+ return
1216
+ if not body and not trailers:
1217
+ state.send_end_stream()
1218
+ self._finalize_stream_if_complete(stream_id)
1219
+ return
1220
+ if not body and trailers:
1221
+ trailer_block = self.hpack_encoder.encode_header_block(trailers)
1222
+ await self._write_raw(self.frame_writer.headers(stream_id, trailer_block, end_stream=True))
1223
+ state.send_end_stream()
1224
+ self._finalize_stream_if_complete(stream_id)
1225
+ return
1226
+ offset = 0
1227
+ while offset < len(body):
1228
+ chunk_size = min(self.state.max_frame_size, len(body) - offset)
1229
+ while self.state.connection_send_window.available <= 0 or state.send_window.available <= 0:
1230
+ await self._wait_for_credit(stream_id)
1231
+ allowed = min(chunk_size, self.state.connection_send_window.available, state.send_window.available)
1232
+ if allowed <= 0:
1233
+ await self._wait_for_credit(stream_id)
1234
+ continue
1235
+ chunk = body[offset : offset + allowed]
1236
+ offset += len(chunk)
1237
+ self.state.connection_send_window.consume(len(chunk))
1238
+ state.send_window.consume(len(chunk))
1239
+ final_chunk = offset == len(body)
1240
+ end_stream = final_chunk and not trailers
1241
+ await self._write_raw(self.frame_writer.data(stream_id, chunk, end_stream=end_stream))
1242
+ if final_chunk and trailers:
1243
+ trailer_block = self.hpack_encoder.encode_header_block(trailers)
1244
+ await self._write_raw(self.frame_writer.headers(stream_id, trailer_block, end_stream=True))
1245
+ state.send_end_stream()
1246
+ self._finalize_stream_if_complete(stream_id)
1247
+ elif final_chunk:
1248
+ state.send_end_stream()
1249
+ self._finalize_stream_if_complete(stream_id)
1250
+
1251
+ async def _wait_for_credit(self, stream_id: int) -> None:
1252
+ state = self.streams.find(stream_id)
1253
+ if state is None or state.closed:
1254
+ raise ProtocolError("attempted to wait for flow-control credit on a closed stream")
1255
+ waiter = self.waiters.setdefault(stream_id, FlowWaiter(state.send_window))
1256
+ waiter.notify()
1257
+ while self.state.connection_send_window.available <= 0 or state.send_window.available <= 0:
1258
+ await waiter.wait()
1259
+ state = self.streams.find(stream_id)
1260
+ if state is None or state.closed:
1261
+ raise ProtocolError("stream closed while waiting for flow-control credit")
1262
+
1263
+ async def _write_raw(self, data: bytes, *, record_activity: bool = True) -> None:
1264
+ async with self.writer_lock:
1265
+ self.writer.write(data)
1266
+ await self.writer.drain()
1267
+ if record_activity:
1268
+ self._record_keepalive_activity()
1269
+
1270
+ def _notify_waiter(self, stream_id: int) -> None:
1271
+ if stream_id == 0:
1272
+ for waiter in self.waiters.values():
1273
+ waiter.notify()
1274
+ return
1275
+ waiter = self.waiters.get(stream_id)
1276
+ if waiter is not None:
1277
+ waiter.notify()
1278
+
1279
+ def _cancel_stream(self, stream_id: int) -> None:
1280
+ self._release_stream_work_lease(stream_id)
1281
+ task = self.stream_tasks.pop(stream_id, None)
1282
+ if task is not None:
1283
+ task.cancel()
1284
+ self.waiters.pop(stream_id, None)
1285
+
1286
+ async def _shutdown_streams(self) -> None:
1287
+ for state in list(self.streams.streams.values()):
1288
+ if state.websocket_session is not None:
1289
+ with suppress(Exception):
1290
+ await state.websocket_session.abort()
1291
+ if state.connect_tunnel is not None:
1292
+ with suppress(Exception):
1293
+ await state.connect_tunnel.abort()
1294
+ for stream_id, task in list(self.stream_tasks.items()):
1295
+ task.cancel()
1296
+ with suppress(asyncio.CancelledError):
1297
+ await task
1298
+ self.stream_tasks.pop(stream_id, None)
1299
+ if not self.state.local_goaway_sent:
1300
+ self.state.local_goaway_sent = True
1301
+ self.state.local_goaway_last_stream_id = self.state.last_stream_id
1302
+ with suppress(Exception):
1303
+ await self._write_raw(serialize_goaway(self.state.last_stream_id))