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,657 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ from tigrcorn_core.errors import ProtocolError
6
+ from tigrcorn_protocols.http3.codec import (
7
+ FRAME_CANCEL_PUSH,
8
+ FRAME_DATA,
9
+ FRAME_GOAWAY,
10
+ FRAME_HEADERS,
11
+ FRAME_MAX_PUSH_ID,
12
+ FRAME_PUSH_PROMISE,
13
+ FRAME_SETTINGS,
14
+ H3_CLOSED_CRITICAL_STREAM,
15
+ H3_EXCESSIVE_LOAD,
16
+ H3_FRAME_ERROR,
17
+ H3_FRAME_UNEXPECTED,
18
+ H3_GENERAL_PROTOCOL_ERROR,
19
+ H3_ID_ERROR,
20
+ H3_MESSAGE_ERROR,
21
+ H3_MISSING_SETTINGS,
22
+ H3_REQUEST_INCOMPLETE,
23
+ H3_REQUEST_REJECTED,
24
+ H3_SETTINGS_ERROR,
25
+ H3_STREAM_CREATION_ERROR,
26
+ HTTP3ConnectionError,
27
+ HTTP3StreamError,
28
+ QPACK_DECODER_STREAM_ERROR,
29
+ QPACK_DECOMPRESSION_FAILED,
30
+ QPACK_ENCODER_STREAM_ERROR,
31
+ STREAM_TYPE_CONTROL,
32
+ decode_frame,
33
+ decode_settings,
34
+ decode_single_varint,
35
+ encode_frame,
36
+ encode_settings,
37
+ )
38
+ from tigrcorn_protocols.http3.qpack import (
39
+ QpackBlocked,
40
+ QpackDecoder,
41
+ QpackDecoderStreamError,
42
+ QpackDecompressionFailed,
43
+ QpackEncoder,
44
+ QpackEncoderStreamError,
45
+ decode_field_section,
46
+ encode_field_section,
47
+ )
48
+ from tigrcorn_protocols.http3.state import (
49
+ HTTP3BlockedSection,
50
+ HTTP3ConnectionState,
51
+ HTTP3PushPromiseState,
52
+ HTTP3RequestPhase_DATA,
53
+ HTTP3RequestPhase_INITIAL,
54
+ HTTP3RequestPhase_TRAILERS,
55
+ HTTP3RequestState,
56
+ HTTP3UniStreamState,
57
+ )
58
+ from tigrcorn_core.utils.bytes import decode_quic_varint, encode_quic_varint
59
+
60
+ HTTP3_STREAM_PRESSURE_CERTIFICATION_SCOPES: tuple[str, ...] = ('stream-level-backpressure', 'connection-level-backpressure', 'goaway-pressure')
61
+ DEFAULT_HTTP3_REQUEST_PARSE_BUFFER_LIMIT = 65_536
62
+
63
+
64
+ def supported_http3_stream_pressure_certification_scopes() -> tuple[str, ...]:
65
+ return HTTP3_STREAM_PRESSURE_CERTIFICATION_SCOPES
66
+
67
+ STREAM_TYPE_PUSH = 0x01
68
+ STREAM_TYPE_QPACK_ENCODER = 0x02
69
+ STREAM_TYPE_QPACK_DECODER = 0x03
70
+ SETTING_QPACK_MAX_TABLE_CAPACITY = 0x01
71
+ SETTING_MAX_FIELD_SECTION_SIZE = 0x06
72
+ SETTING_QPACK_BLOCKED_STREAMS = 0x07
73
+ _REQUEST_STATE_INITIAL = HTTP3RequestPhase_INITIAL
74
+ _REQUEST_STATE_DATA = HTTP3RequestPhase_DATA
75
+ _REQUEST_STATE_TRAILERS = HTTP3RequestPhase_TRAILERS
76
+
77
+
78
+ def _header_section_size(headers: list[tuple[bytes, bytes]]) -> int:
79
+ return sum(len(name) + len(value) + 32 for name, value in headers)
80
+
81
+
82
+
83
+ def _parse_content_length(headers: list[tuple[bytes, bytes]], *, stream_id: int) -> int | None:
84
+ values: list[bytes] = []
85
+ for name, value in headers:
86
+ if name.lower() != b'content-length':
87
+ continue
88
+ for part in value.split(b','):
89
+ values.append(part.strip())
90
+ if not values:
91
+ return None
92
+ parsed: int | None = None
93
+ for value in values:
94
+ if not value or not value.isdigit():
95
+ raise HTTP3StreamError('invalid content-length header', error_code=H3_MESSAGE_ERROR, stream_id=stream_id)
96
+ current = int(value)
97
+ if parsed is None:
98
+ parsed = current
99
+ continue
100
+ if parsed != current:
101
+ raise HTTP3StreamError('conflicting content-length values', error_code=H3_MESSAGE_ERROR, stream_id=stream_id)
102
+ return parsed
103
+
104
+
105
+ def _extract_status_code(headers: list[tuple[bytes, bytes]]) -> int | None:
106
+ for name, value in headers:
107
+ if name != b':status':
108
+ continue
109
+ if not value.isdigit():
110
+ return None
111
+ return int(value)
112
+ return None
113
+
114
+
115
+ def _control_sender_is_client(stream_id: int) -> bool:
116
+ return (stream_id & 0x01) == 0
117
+
118
+
119
+ @dataclass(slots=True)
120
+ class HTTP3RequestStream:
121
+ state: HTTP3RequestState
122
+ qpack_encoder: QpackEncoder | None = None
123
+ qpack_decoder: QpackDecoder | None = None
124
+ connection_state: HTTP3ConnectionState | None = None
125
+ role: str | None = None
126
+ parse_buffer_limit: int = DEFAULT_HTTP3_REQUEST_PARSE_BUFFER_LIMIT
127
+
128
+ def encode_request(self, headers: list[tuple[bytes, bytes]], body: bytes = b'') -> bytes:
129
+ raw = bytearray()
130
+ if self.qpack_encoder is not None:
131
+ header_block = self.qpack_encoder.encode_field_section(headers, stream_id=self.state.stream_id)
132
+ else:
133
+ header_block = encode_field_section(headers)
134
+ raw.extend(encode_frame(FRAME_HEADERS, header_block))
135
+ if body:
136
+ raw.extend(encode_frame(FRAME_DATA, body))
137
+ return bytes(raw)
138
+
139
+ def _max_field_section_size(self) -> int | None:
140
+ if self.connection_state is None:
141
+ return None
142
+ limit = self.connection_state.local_settings.get(SETTING_MAX_FIELD_SECTION_SIZE)
143
+ if limit is None or limit <= 0:
144
+ return None
145
+ return limit
146
+
147
+ def _decode_field_section_payload(self, payload: bytes) -> list[tuple[bytes, bytes]]:
148
+ if self.qpack_decoder is None:
149
+ return decode_field_section(payload)
150
+ try:
151
+ field_section = self.qpack_decoder.decode_field_section(payload, stream_id=self.state.stream_id)
152
+ except QpackBlocked as exc:
153
+ raise exc
154
+ except QpackDecompressionFailed as exc:
155
+ raise HTTP3ConnectionError('invalid HTTP/3 field section', error_code=QPACK_DECOMPRESSION_FAILED) from exc
156
+ except ProtocolError as exc:
157
+ raise HTTP3ConnectionError('invalid HTTP/3 field section', error_code=QPACK_DECOMPRESSION_FAILED) from exc
158
+ return field_section.headers
159
+
160
+ def _queue_blocked_section(self, *, kind: str, payload: bytes, push_id: int | None = None) -> None:
161
+ self.state.blocked_header_sections.append(HTTP3BlockedSection(kind=kind, payload=payload, push_id=push_id))
162
+
163
+ def _enforce_parse_buffer_limit(self) -> None:
164
+ if self.parse_buffer_limit <= 0:
165
+ return
166
+ observed = len(self.state.parse_buffer)
167
+ if observed <= self.parse_buffer_limit:
168
+ return
169
+ self.abandon()
170
+ raise HTTP3StreamError(
171
+ 'HTTP/3 request stream parse buffer limit exceeded',
172
+ error_code=H3_EXCESSIVE_LOAD,
173
+ stream_id=self.state.stream_id,
174
+ )
175
+
176
+ def _enforce_field_section_size(self, headers: list[tuple[bytes, bytes]]) -> None:
177
+ limit = self._max_field_section_size()
178
+ if limit is None:
179
+ return
180
+ if _header_section_size(headers) > limit:
181
+ raise HTTP3StreamError(
182
+ 'HTTP/3 field section exceeds advertised size',
183
+ error_code=H3_MESSAGE_ERROR,
184
+ stream_id=self.state.stream_id,
185
+ )
186
+
187
+ def _apply_initial_headers(self, headers: list[tuple[bytes, bytes]]) -> None:
188
+ self._enforce_field_section_size(headers)
189
+ status_code = _extract_status_code(headers)
190
+ if self.role == 'client' and status_code is not None and 100 <= status_code < 200:
191
+ self.state.informational_headers.append(list(headers))
192
+ return
193
+ self.state.headers.extend(headers)
194
+ self.state.received_initial_headers = True
195
+ self.state.phase = _REQUEST_STATE_DATA
196
+ content_length = _parse_content_length(headers, stream_id=self.state.stream_id)
197
+ if content_length is not None:
198
+ self.state.expected_content_length = content_length
199
+ if self.state.received_content_length > content_length:
200
+ raise HTTP3StreamError(
201
+ 'request body exceeds content-length',
202
+ error_code=H3_MESSAGE_ERROR,
203
+ stream_id=self.state.stream_id,
204
+ )
205
+
206
+ def _apply_trailers(self, headers: list[tuple[bytes, bytes]]) -> None:
207
+ self._enforce_field_section_size(headers)
208
+ for name, _value in headers:
209
+ if name.startswith(b':'):
210
+ raise HTTP3StreamError(
211
+ 'pseudo-header field in trailer section',
212
+ error_code=H3_MESSAGE_ERROR,
213
+ stream_id=self.state.stream_id,
214
+ )
215
+ self.state.trailers.extend(headers)
216
+ self.state.received_trailers = True
217
+ self.state.phase = _REQUEST_STATE_TRAILERS
218
+
219
+ def _store_push_promise(self, push_id: int, headers: list[tuple[bytes, bytes]]) -> None:
220
+ connection_state = self.connection_state
221
+ if connection_state is None:
222
+ connection_state = HTTP3ConnectionState()
223
+ self.connection_state = connection_state
224
+ max_push_id = connection_state.local_max_push_id
225
+ if max_push_id is None or push_id > max_push_id:
226
+ raise HTTP3ConnectionError('PUSH_PROMISE exceeds advertised MAX_PUSH_ID', error_code=H3_ID_ERROR)
227
+ existing = connection_state.promised_pushes.get(push_id)
228
+ if existing is not None:
229
+ if existing.headers != headers:
230
+ raise HTTP3ConnectionError(
231
+ 'inconsistent duplicate PUSH_PROMISE field section',
232
+ error_code=H3_GENERAL_PROTOCOL_ERROR,
233
+ )
234
+ existing.request_stream_ids.add(self.state.stream_id)
235
+ self.state.push_promises[push_id] = existing
236
+ return
237
+ promise = HTTP3PushPromiseState(push_id=push_id, headers=list(headers), request_stream_ids={self.state.stream_id})
238
+ connection_state.promised_pushes[push_id] = promise
239
+ self.state.push_promises[push_id] = promise
240
+
241
+ def _apply_blocked_section(self, section: HTTP3BlockedSection) -> None:
242
+ try:
243
+ headers = self._decode_field_section_payload(section.payload)
244
+ except QpackBlocked:
245
+ raise
246
+ if section.kind == 'initial':
247
+ self._apply_initial_headers(headers)
248
+ return
249
+ if section.kind == 'trailers':
250
+ self._apply_trailers(headers)
251
+ return
252
+ if section.kind == 'push':
253
+ assert section.push_id is not None
254
+ self._store_push_promise(section.push_id, headers)
255
+ return
256
+ raise HTTP3ConnectionError('unknown blocked header section kind', error_code=H3_GENERAL_PROTOCOL_ERROR)
257
+
258
+ def _decode_or_block(self, *, kind: str, payload: bytes, push_id: int | None = None) -> bool:
259
+ try:
260
+ headers = self._decode_field_section_payload(payload)
261
+ except QpackBlocked:
262
+ self._queue_blocked_section(kind=kind, payload=payload, push_id=push_id)
263
+ return True
264
+ if kind == 'initial':
265
+ self._apply_initial_headers(headers)
266
+ return False
267
+ if kind == 'trailers':
268
+ self._apply_trailers(headers)
269
+ return False
270
+ if kind == 'push':
271
+ assert push_id is not None
272
+ self._store_push_promise(push_id, headers)
273
+ return False
274
+ raise HTTP3ConnectionError('unknown header section kind', error_code=H3_GENERAL_PROTOCOL_ERROR)
275
+
276
+ def _handle_headers_frame(self, payload: bytes) -> bool:
277
+ if self.state.phase == _REQUEST_STATE_INITIAL:
278
+ return self._decode_or_block(kind='initial', payload=payload)
279
+ if self.state.phase == _REQUEST_STATE_DATA:
280
+ return self._decode_or_block(kind='trailers', payload=payload)
281
+ raise HTTP3ConnectionError('HEADERS after trailer section', error_code=H3_FRAME_UNEXPECTED)
282
+
283
+ def _handle_data_frame(self, payload: bytes) -> bool:
284
+ if self.state.phase == _REQUEST_STATE_INITIAL:
285
+ raise HTTP3ConnectionError('DATA frame before initial HEADERS', error_code=H3_FRAME_UNEXPECTED)
286
+ if self.state.phase == _REQUEST_STATE_TRAILERS:
287
+ raise HTTP3ConnectionError('DATA frame after trailing HEADERS', error_code=H3_FRAME_UNEXPECTED)
288
+ self.state.body_parts.append(payload)
289
+ self.state.received_content_length += len(payload)
290
+ expected = self.state.expected_content_length
291
+ if expected is not None and self.state.received_content_length > expected:
292
+ raise HTTP3StreamError(
293
+ 'request body exceeds content-length',
294
+ error_code=H3_MESSAGE_ERROR,
295
+ stream_id=self.state.stream_id,
296
+ )
297
+ return False
298
+
299
+ def _handle_push_promise_frame(self, payload: bytes) -> bool:
300
+ if self.role == 'server':
301
+ raise HTTP3ConnectionError('server received PUSH_PROMISE on request stream', error_code=H3_FRAME_UNEXPECTED)
302
+ try:
303
+ push_id, offset = decode_quic_varint(payload, 0)
304
+ except ProtocolError as exc:
305
+ raise HTTP3ConnectionError('malformed PUSH_PROMISE frame payload', error_code=H3_FRAME_ERROR) from exc
306
+ field_section = payload[offset:]
307
+ return self._decode_or_block(kind='push', payload=field_section, push_id=push_id)
308
+
309
+ def _handle_frame(self, frame_type: int, payload: bytes) -> bool:
310
+ if frame_type == FRAME_HEADERS:
311
+ return self._handle_headers_frame(payload)
312
+ if frame_type == FRAME_DATA:
313
+ return self._handle_data_frame(payload)
314
+ if frame_type == FRAME_PUSH_PROMISE:
315
+ return self._handle_push_promise_frame(payload)
316
+ if frame_type in {FRAME_CANCEL_PUSH, FRAME_SETTINGS, FRAME_GOAWAY, FRAME_MAX_PUSH_ID}:
317
+ raise HTTP3ConnectionError('frame not permitted on request stream', error_code=H3_FRAME_UNEXPECTED)
318
+ return False
319
+
320
+ def _process_parse_buffer(self) -> None:
321
+ self._enforce_parse_buffer_limit()
322
+ offset = 0
323
+ data = bytes(self.state.parse_buffer)
324
+ while offset < len(data):
325
+ try:
326
+ frame, next_offset = decode_frame(data, offset)
327
+ except ProtocolError:
328
+ break
329
+ offset = next_offset
330
+ blocked = self._handle_frame(frame.frame_type, frame.payload)
331
+ if blocked:
332
+ break
333
+ remaining = data[offset:]
334
+ self.state.parse_buffer.clear()
335
+ self.state.parse_buffer.extend(remaining)
336
+ self._enforce_parse_buffer_limit()
337
+
338
+ def _finalize_complete_message(self) -> None:
339
+ if not self.state.ended:
340
+ return
341
+ if self.state.blocked_header_sections:
342
+ return
343
+ if self.state.parse_buffer:
344
+ raise HTTP3StreamError(
345
+ 'request stream ended with incomplete frame',
346
+ error_code=H3_REQUEST_INCOMPLETE,
347
+ stream_id=self.state.stream_id,
348
+ )
349
+ if not self.state.received_initial_headers:
350
+ raise HTTP3StreamError(
351
+ 'request stream ended before initial HEADERS',
352
+ error_code=H3_REQUEST_INCOMPLETE,
353
+ stream_id=self.state.stream_id,
354
+ )
355
+ expected = self.state.expected_content_length
356
+ if expected is not None and self.state.received_content_length != expected:
357
+ raise HTTP3StreamError(
358
+ 'content-length does not match DATA frame lengths',
359
+ error_code=H3_MESSAGE_ERROR,
360
+ stream_id=self.state.stream_id,
361
+ )
362
+
363
+ def retry_blocked(self) -> bool:
364
+ if self.qpack_decoder is None or not self.state.blocked_header_sections:
365
+ self._finalize_complete_message()
366
+ return False
367
+ progressed = False
368
+ remaining: list[HTTP3BlockedSection] = []
369
+ for section in self.state.blocked_header_sections:
370
+ try:
371
+ self._apply_blocked_section(section)
372
+ except QpackBlocked:
373
+ remaining.append(section)
374
+ continue
375
+ progressed = True
376
+ self.state.blocked_header_sections = remaining
377
+ if progressed and not self.state.blocked_header_sections and self.state.parse_buffer:
378
+ self._process_parse_buffer()
379
+ self._finalize_complete_message()
380
+ return progressed
381
+
382
+ def abandon(self) -> None:
383
+ if self.state.abandoned:
384
+ return
385
+ self.state.abandoned = True
386
+ if self.qpack_decoder is not None and self.state.blocked_header_sections:
387
+ self.qpack_decoder.cancel_stream(self.state.stream_id)
388
+ self.state.blocked_header_sections.clear()
389
+ self.state.parse_buffer.clear()
390
+
391
+ def receive(self, payload: bytes, *, fin: bool = False) -> HTTP3RequestState:
392
+ if self.state.abandoned:
393
+ return self.state
394
+ self.state.parse_buffer.extend(payload)
395
+ self._enforce_parse_buffer_limit()
396
+ if fin:
397
+ self.state.ended = True
398
+ self._process_parse_buffer()
399
+ self.retry_blocked()
400
+ self._finalize_complete_message()
401
+ return self.state
402
+
403
+
404
+ @dataclass(slots=True)
405
+ class HTTP3ConnectionCore:
406
+ state: HTTP3ConnectionState = field(default_factory=HTTP3ConnectionState)
407
+ role: str | None = None
408
+ requests: dict[int, HTTP3RequestStream] = field(default_factory=dict)
409
+ qpack_encoder: QpackEncoder = field(default_factory=QpackEncoder)
410
+ qpack_decoder: QpackDecoder = field(default_factory=QpackDecoder)
411
+ max_request_parse_buffer_size: int = DEFAULT_HTTP3_REQUEST_PARSE_BUFFER_LIMIT
412
+
413
+ def _update_request_codecs(self) -> None:
414
+ for request in self.requests.values():
415
+ request.qpack_encoder = self.qpack_encoder
416
+ request.qpack_decoder = self.qpack_decoder
417
+ request.connection_state = self.state
418
+ request.role = self.role
419
+
420
+ def _configure_local_decoder(self) -> None:
421
+ capacity = self.state.local_settings.get(SETTING_QPACK_MAX_TABLE_CAPACITY, 0)
422
+ blocked = self.state.local_settings.get(SETTING_QPACK_BLOCKED_STREAMS, 0)
423
+ self.qpack_decoder = QpackDecoder(max_table_capacity=capacity, blocked_streams=blocked)
424
+ self._update_request_codecs()
425
+
426
+ def _configure_remote_encoder(self) -> None:
427
+ capacity = self.state.remote_settings.get(SETTING_QPACK_MAX_TABLE_CAPACITY, 0)
428
+ blocked = self.state.remote_settings.get(SETTING_QPACK_BLOCKED_STREAMS, 0)
429
+ self.qpack_encoder = QpackEncoder(max_table_capacity=capacity, blocked_streams=blocked)
430
+ self._update_request_codecs()
431
+
432
+ def encode_control_stream(self, settings: dict[int, int]) -> bytes:
433
+ if self.state.control_stream_opened:
434
+ raise ProtocolError('HTTP/3 endpoints must not open multiple local control streams')
435
+ self.state.local_settings.update(settings)
436
+ self.state.control_stream_opened = True
437
+ self._configure_local_decoder()
438
+ return encode_quic_varint(STREAM_TYPE_CONTROL) + encode_frame(FRAME_SETTINGS, encode_settings(settings))
439
+
440
+ def encode_goaway(self, identifier: int) -> bytes:
441
+ if self.state.local_goaway_id is not None and identifier > self.state.local_goaway_id:
442
+ raise ProtocolError('HTTP/3 GOAWAY identifier must not increase')
443
+ self.state.local_goaway_id = identifier
444
+ self.state.goaway_stream_id = identifier
445
+ return encode_frame(FRAME_GOAWAY, encode_quic_varint(identifier))
446
+
447
+ def encode_cancel_push(self, push_id: int) -> bytes:
448
+ return encode_frame(FRAME_CANCEL_PUSH, encode_quic_varint(push_id))
449
+
450
+ def encode_max_push_id(self, push_id: int) -> bytes:
451
+ if self.state.local_max_push_id is not None and push_id < self.state.local_max_push_id:
452
+ raise ProtocolError('HTTP/3 MAX_PUSH_ID must not decrease')
453
+ self.state.local_max_push_id = push_id
454
+ return encode_frame(FRAME_MAX_PUSH_ID, encode_quic_varint(push_id))
455
+
456
+ def encode_push_promise(self, stream_id: int, push_id: int, headers: list[tuple[bytes, bytes]]) -> bytes:
457
+ if self.qpack_encoder is not None:
458
+ header_block = self.qpack_encoder.encode_field_section(headers, stream_id=stream_id)
459
+ else:
460
+ header_block = encode_field_section(headers)
461
+ payload = encode_quic_varint(push_id) + header_block
462
+ return encode_frame(FRAME_PUSH_PROMISE, payload)
463
+
464
+ def get_request(self, stream_id: int) -> HTTP3RequestStream:
465
+ return self.requests.setdefault(
466
+ stream_id,
467
+ HTTP3RequestStream(
468
+ state=HTTP3RequestState(stream_id=stream_id),
469
+ qpack_encoder=self.qpack_encoder,
470
+ qpack_decoder=self.qpack_decoder,
471
+ connection_state=self.state,
472
+ role=self.role,
473
+ parse_buffer_limit=self.max_request_parse_buffer_size,
474
+ ),
475
+ )
476
+
477
+ def abandon_stream(self, stream_id: int) -> None:
478
+ request = self.requests.get(stream_id)
479
+ if request is None:
480
+ return
481
+ request.abandon()
482
+
483
+ def encode_headers(self, stream_id: int, headers: list[tuple[bytes, bytes]]) -> bytes:
484
+ return self.qpack_encoder.encode_field_section(headers, stream_id=stream_id)
485
+
486
+ def take_encoder_stream_data(self) -> bytes:
487
+ return self.qpack_encoder.take_encoder_stream_data()
488
+
489
+ def take_decoder_stream_data(self) -> bytes:
490
+ return self.qpack_decoder.take_decoder_stream_data()
491
+
492
+ def ready_request_states(self) -> list[HTTP3RequestState]:
493
+ return [request.state for request in self.requests.values() if request.state.ready]
494
+
495
+ def _retry_blocked_requests(self) -> None:
496
+ for request in self.requests.values():
497
+ request.retry_blocked()
498
+
499
+ def _process_goaway(self, identifier: int, *, sender_is_client: bool) -> None:
500
+ direction = 'client' if sender_is_client else 'server'
501
+ previous = self.state.peer_goaway_id
502
+ if previous is not None and identifier > previous:
503
+ raise HTTP3ConnectionError('GOAWAY identifier increased', error_code=H3_ID_ERROR)
504
+ if not sender_is_client and (identifier & 0x03) != 0:
505
+ raise HTTP3ConnectionError('server GOAWAY identifier must be a client-initiated bidirectional stream ID', error_code=H3_ID_ERROR)
506
+ self.state.peer_goaway_direction = direction
507
+ self.state.peer_goaway_id = identifier
508
+ self.state.goaway_stream_id = identifier
509
+
510
+ def _process_cancel_push(self, push_id: int, *, sender_is_client: bool) -> None:
511
+ if sender_is_client:
512
+ max_push_id = self.state.peer_max_push_id
513
+ if max_push_id is not None and push_id > max_push_id:
514
+ raise HTTP3ConnectionError('CANCEL_PUSH exceeds peer MAX_PUSH_ID', error_code=H3_ID_ERROR)
515
+ if push_id not in self.state.promised_pushes:
516
+ raise HTTP3ConnectionError('CANCEL_PUSH references unknown promised push', error_code=H3_ID_ERROR)
517
+ else:
518
+ local_max_push_id = self.state.local_max_push_id
519
+ if local_max_push_id is not None and push_id > local_max_push_id:
520
+ raise HTTP3ConnectionError('CANCEL_PUSH exceeds advertised MAX_PUSH_ID', error_code=H3_ID_ERROR)
521
+ self.state.cancelled_push_ids.add(push_id)
522
+
523
+ def _process_max_push_id(self, push_id: int, *, sender_is_client: bool) -> None:
524
+ if not sender_is_client:
525
+ raise HTTP3ConnectionError('server sent MAX_PUSH_ID', error_code=H3_FRAME_UNEXPECTED)
526
+ if self.state.peer_max_push_id is not None and push_id < self.state.peer_max_push_id:
527
+ raise HTTP3ConnectionError('MAX_PUSH_ID must not decrease', error_code=H3_ID_ERROR)
528
+ self.state.peer_max_push_id = push_id
529
+
530
+ def _receive_control_frame(self, frame_type: int, payload: bytes, *, stream_id: int) -> None:
531
+ sender_is_client = _control_sender_is_client(stream_id)
532
+ if frame_type == FRAME_SETTINGS:
533
+ raise HTTP3ConnectionError('duplicate HTTP/3 SETTINGS frame', error_code=H3_FRAME_UNEXPECTED)
534
+ if frame_type == FRAME_GOAWAY:
535
+ identifier = decode_single_varint(payload, context='GOAWAY')
536
+ self._process_goaway(identifier, sender_is_client=sender_is_client)
537
+ return
538
+ if frame_type == FRAME_CANCEL_PUSH:
539
+ push_id = decode_single_varint(payload, context='CANCEL_PUSH')
540
+ self._process_cancel_push(push_id, sender_is_client=sender_is_client)
541
+ return
542
+ if frame_type == FRAME_MAX_PUSH_ID:
543
+ push_id = decode_single_varint(payload, context='MAX_PUSH_ID')
544
+ self._process_max_push_id(push_id, sender_is_client=sender_is_client)
545
+ return
546
+ if frame_type in {FRAME_PUSH_PROMISE, FRAME_HEADERS, FRAME_DATA}:
547
+ raise HTTP3ConnectionError('frame not permitted on control stream', error_code=H3_FRAME_UNEXPECTED)
548
+ # Unknown or reserved frame types are ignored after SETTINGS.
549
+
550
+ def _receive_uni_stream(self, stream_id: int, payload: bytes, *, fin: bool = False) -> None:
551
+ state = self.state.uni_streams.setdefault(stream_id, HTTP3UniStreamState(stream_id=stream_id))
552
+ state.parse_buffer.extend(payload)
553
+ offset = 0
554
+ data = bytes(state.parse_buffer)
555
+ if state.stream_type is None:
556
+ try:
557
+ state.stream_type, offset = decode_quic_varint(data, offset)
558
+ except ProtocolError:
559
+ if fin:
560
+ state.parse_buffer.clear()
561
+ return
562
+ if state.stream_type == STREAM_TYPE_CONTROL:
563
+ if self.state.remote_control_stream_id is None:
564
+ self.state.remote_control_stream_id = stream_id
565
+ elif self.state.remote_control_stream_id != stream_id:
566
+ raise HTTP3ConnectionError('peer opened more than one control stream', error_code=H3_STREAM_CREATION_ERROR)
567
+ elif state.stream_type == STREAM_TYPE_QPACK_ENCODER:
568
+ if self.state.remote_qpack_encoder_stream_id is None:
569
+ self.state.remote_qpack_encoder_stream_id = stream_id
570
+ elif self.state.remote_qpack_encoder_stream_id != stream_id:
571
+ raise HTTP3ConnectionError('peer opened more than one QPACK encoder stream', error_code=H3_STREAM_CREATION_ERROR)
572
+ elif state.stream_type == STREAM_TYPE_QPACK_DECODER:
573
+ if self.state.remote_qpack_decoder_stream_id is None:
574
+ self.state.remote_qpack_decoder_stream_id = stream_id
575
+ elif self.state.remote_qpack_decoder_stream_id != stream_id:
576
+ raise HTTP3ConnectionError('peer opened more than one QPACK decoder stream', error_code=H3_STREAM_CREATION_ERROR)
577
+ elif state.stream_type == STREAM_TYPE_PUSH:
578
+ if _control_sender_is_client(stream_id):
579
+ raise HTTP3ConnectionError('client-initiated push stream is forbidden', error_code=H3_STREAM_CREATION_ERROR)
580
+ self.state.remote_push_stream_ids.add(stream_id)
581
+ else:
582
+ state.discard_stream = True
583
+ if state.stream_type == STREAM_TYPE_CONTROL:
584
+ if fin:
585
+ raise HTTP3ConnectionError('HTTP/3 control stream closed', error_code=H3_CLOSED_CRITICAL_STREAM)
586
+ while offset < len(data):
587
+ try:
588
+ frame, next_offset = decode_frame(data, offset)
589
+ except ProtocolError:
590
+ break
591
+ offset = next_offset
592
+ if not state.settings_received:
593
+ if frame.frame_type != FRAME_SETTINGS:
594
+ raise HTTP3ConnectionError('control stream must begin with SETTINGS', error_code=H3_MISSING_SETTINGS)
595
+ state.settings_received = True
596
+ try:
597
+ self.state.remote_settings.update(decode_settings(frame.payload))
598
+ except HTTP3ConnectionError:
599
+ raise
600
+ except ProtocolError as exc:
601
+ raise HTTP3ConnectionError('invalid HTTP/3 SETTINGS payload', error_code=H3_SETTINGS_ERROR) from exc
602
+ self._configure_remote_encoder()
603
+ continue
604
+ self._receive_control_frame(frame.frame_type, frame.payload, stream_id=stream_id)
605
+ remaining = data[offset:]
606
+ state.parse_buffer.clear()
607
+ state.parse_buffer.extend(remaining)
608
+ return
609
+ if state.stream_type == STREAM_TYPE_QPACK_ENCODER:
610
+ if fin:
611
+ raise HTTP3ConnectionError('HTTP/3 QPACK encoder stream closed', error_code=H3_CLOSED_CRITICAL_STREAM)
612
+ try:
613
+ self.qpack_decoder.receive_encoder_stream(data[offset:])
614
+ except QpackEncoderStreamError as exc:
615
+ raise HTTP3ConnectionError('invalid QPACK encoder stream data', error_code=QPACK_ENCODER_STREAM_ERROR) from exc
616
+ except ProtocolError as exc:
617
+ raise HTTP3ConnectionError('invalid QPACK encoder stream data', error_code=QPACK_ENCODER_STREAM_ERROR) from exc
618
+ finally:
619
+ state.parse_buffer.clear()
620
+ self._retry_blocked_requests()
621
+ return
622
+ if state.stream_type == STREAM_TYPE_QPACK_DECODER:
623
+ if fin:
624
+ raise HTTP3ConnectionError('HTTP/3 QPACK decoder stream closed', error_code=H3_CLOSED_CRITICAL_STREAM)
625
+ try:
626
+ self.qpack_encoder.receive_decoder_stream(data[offset:])
627
+ except QpackDecoderStreamError as exc:
628
+ raise HTTP3ConnectionError('invalid QPACK decoder stream data', error_code=QPACK_DECODER_STREAM_ERROR) from exc
629
+ except ProtocolError as exc:
630
+ raise HTTP3ConnectionError('invalid QPACK decoder stream data', error_code=QPACK_DECODER_STREAM_ERROR) from exc
631
+ finally:
632
+ state.parse_buffer.clear()
633
+ return
634
+ if state.stream_type == STREAM_TYPE_PUSH:
635
+ if state.push_id is None:
636
+ try:
637
+ state.push_id, offset = decode_quic_varint(data, offset)
638
+ except ProtocolError:
639
+ if fin:
640
+ state.parse_buffer.clear()
641
+ return
642
+ for other in self.state.uni_streams.values():
643
+ if other is state or other.stream_type != STREAM_TYPE_PUSH or other.push_id is None:
644
+ continue
645
+ if other.push_id == state.push_id:
646
+ raise HTTP3ConnectionError('push stream reused push ID', error_code=H3_ID_ERROR)
647
+ state.parse_buffer.clear()
648
+ return
649
+ state.parse_buffer.clear()
650
+
651
+ def receive_stream_data(self, stream_id: int, payload: bytes, *, fin: bool = False) -> HTTP3RequestState | None:
652
+ if stream_id & 0x02:
653
+ self._receive_uni_stream(stream_id, payload, fin=fin)
654
+ return None
655
+ if self.role == 'server' and self.state.local_goaway_id is not None and stream_id >= self.state.local_goaway_id and stream_id not in self.requests:
656
+ raise HTTP3StreamError('request rejected after GOAWAY', error_code=H3_REQUEST_REJECTED, stream_id=stream_id)
657
+ return self.get_request(stream_id).receive(payload, fin=fin)