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,360 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from contextlib import suppress
5
+ from typing import Awaitable, Callable
6
+
7
+ from tigrcorn_protocols.flow.keepalive import KeepAlivePolicy, KeepAliveRuntime
8
+ from tigrcorn_observability.metrics import Metrics
9
+
10
+ from tigrcorn_asgi.events.websocket import websocket_connect, websocket_disconnect, websocket_receive_bytes, websocket_receive_text
11
+ from tigrcorn_asgi.receive import QueueReceive
12
+ from tigrcorn_asgi.scopes.websocket import build_websocket_scope
13
+ from tigrcorn_config.model import ServerConfig
14
+ from tigrcorn_core.errors import ProtocolError
15
+ from tigrcorn_protocols.http1.parser import ParsedRequest
16
+ from tigrcorn_protocols.websocket.codec import binary_frame, close_frame, pong_frame, text_frame
17
+ from tigrcorn_protocols.websocket.frames import OP_BINARY, OP_CLOSE, OP_CONT, OP_PING, OP_PONG, OP_TEXT, decode_close_payload, parse_frame_bytes, serialize_frame
18
+ from tigrcorn_protocols.websocket.extensions import PerMessageDeflateRuntime, default_permessage_deflate_agreement, negotiate_permessage_deflate, parse_permessage_deflate_offers
19
+ from tigrcorn_core.types import ASGIApp
20
+ from tigrcorn_core.utils.headers import get_header
21
+
22
+
23
+ class H2WebSocketSession:
24
+ def __init__(
25
+ self,
26
+ *,
27
+ app: ASGIApp,
28
+ config: ServerConfig,
29
+ request: ParsedRequest,
30
+ client: tuple[str, int] | None,
31
+ server: tuple[str, int] | tuple[str, None] | None,
32
+ scheme: str,
33
+ send_headers: Callable[[int, list[tuple[bytes, bytes]], bool], Awaitable[None]],
34
+ send_data: Callable[[bytes, bool], Awaitable[None]],
35
+ metrics: Metrics | None = None,
36
+ on_close: Callable[[], None] | None = None,
37
+ ) -> None:
38
+ self.app = app
39
+ self.config = config
40
+ self.request = request
41
+ self.client = client
42
+ self.server = server
43
+ self.scheme = 'wss' if scheme == 'https' else 'ws'
44
+ self.send_headers = send_headers
45
+ self.send_data = send_data
46
+ self.metrics = metrics
47
+ self.on_close = on_close
48
+ self.receive = QueueReceive(max_size=self.config.websocket.max_queue)
49
+ self.task: asyncio.Task[None] | None = None
50
+ self.accepted = False
51
+ self.closed = False
52
+ self.http_denied = False
53
+ self.http_denial_status = 403
54
+ self.http_denial_headers: list[tuple[bytes, bytes]] = []
55
+ self.http_denial_started = False
56
+ self.subprotocols = build_websocket_scope(request, client=client, server=server, scheme=self.scheme, root_path=self.config.proxy.root_path, proxy=self.config.proxy)['subprotocols']
57
+ self.buffer = bytearray()
58
+ self.peer_end_stream_pending = False
59
+ self.fragmented_opcode: int | None = None
60
+ self.fragments: list[bytes] = []
61
+ self.current_message_size = 0
62
+ self.fragmented_compressed = False
63
+ self.permessage_deflate_offers = parse_permessage_deflate_offers(request.headers)
64
+ self.permessage_deflate_runtime: PerMessageDeflateRuntime | None = None
65
+ self.keepalive_policy = KeepAlivePolicy(
66
+ idle_timeout=self.config.http.idle_timeout,
67
+ ping_interval=self.config.websocket.ping_interval,
68
+ ping_timeout=self.config.websocket.ping_timeout,
69
+ )
70
+ self.keepalive = KeepAliveRuntime(self.keepalive_policy) if self.keepalive_policy.enabled else None
71
+ self.keepalive_task: asyncio.Task[None] | None = None
72
+ version = get_header(request.headers, b'sec-websocket-version')
73
+ if version != b'13':
74
+ raise ProtocolError('unsupported websocket version')
75
+
76
+ async def start(self) -> None:
77
+ scope = build_websocket_scope(self.request, client=self.client, server=self.server, scheme=self.scheme, root_path=self.config.proxy.root_path, proxy=self.config.proxy)
78
+ await self.receive.put(websocket_connect())
79
+ self.task = asyncio.create_task(self._run_app(scope), name=f'tigrcorn-h2-ws-{self.request.path}')
80
+ if self.keepalive is not None:
81
+ self.keepalive_task = asyncio.create_task(self._keepalive_loop(), name=f'tigrcorn-h2-ws-keepalive-{self.request.path}')
82
+
83
+ def _record_activity(self) -> None:
84
+ if self.keepalive is not None:
85
+ self.keepalive.record_activity()
86
+
87
+ def _notify_closed(self) -> None:
88
+ if self.on_close is not None:
89
+ callback = self.on_close
90
+ self.on_close = None
91
+ callback()
92
+
93
+ async def _keepalive_loop(self) -> None:
94
+ while not self.closed:
95
+ await asyncio.sleep(0.05)
96
+ if self.keepalive is None or self.closed:
97
+ return
98
+ if self.keepalive.ping_timed_out():
99
+ if self.metrics is not None:
100
+ self.metrics.websocket_ping_timeout()
101
+ if not self.closed:
102
+ await self.send_data(close_frame(1011, 'ping timeout'), True)
103
+ self.closed = True
104
+ self._notify_closed()
105
+ await self.receive.put(websocket_disconnect(1011, 'ping timeout'))
106
+ return
107
+ payload = self.keepalive.next_ping_payload()
108
+ if payload is None:
109
+ continue
110
+ if self.metrics is not None:
111
+ self.metrics.websocket_ping_sent()
112
+ await self.send_data(serialize_frame(OP_PING, payload), False)
113
+
114
+ async def _run_app(self, scope: dict) -> None:
115
+ try:
116
+ await self.app(scope, self.receive, self._send)
117
+ except Exception:
118
+ if self.accepted and not self.closed:
119
+ with suppress(Exception):
120
+ await self.send_data(close_frame(1011, 'internal error'), True)
121
+ raise
122
+ finally:
123
+ if self.http_denied and not self.closed:
124
+ if not self.http_denial_started:
125
+ await self.send_headers(self.http_denial_status, self.http_denial_headers, True)
126
+ self.http_denial_started = True
127
+ self.closed = True
128
+ elif not self.accepted and not self.closed:
129
+ await self.send_headers(403, [], True)
130
+ self.closed = True
131
+ elif self.accepted and not self.closed:
132
+ await self.send_data(close_frame(1000, ''), True)
133
+ self.closed = True
134
+ self._notify_closed()
135
+ if self.keepalive_task is not None:
136
+ self.keepalive_task.cancel()
137
+ with suppress(asyncio.CancelledError):
138
+ await self.keepalive_task
139
+
140
+ async def _send(self, message: dict) -> None:
141
+ typ = message['type']
142
+ if typ == 'websocket.accept':
143
+ if self.accepted or self.http_denied:
144
+ raise RuntimeError('websocket.accept sent more than once')
145
+ subprotocol = message.get('subprotocol')
146
+ if subprotocol is not None and subprotocol not in self.subprotocols:
147
+ raise RuntimeError('websocket.accept selected a subprotocol not offered by the client')
148
+ headers = [(bytes(k).lower(), bytes(v)) for k, v in message.get('headers', [])]
149
+ if self.config.websocket.compression != 'permessage-deflate':
150
+ headers = [(k, v) for k, v in headers if k != b'sec-websocket-extensions']
151
+ elif self.permessage_deflate_offers and get_header(headers, b'sec-websocket-extensions') is None:
152
+ default_agreement = default_permessage_deflate_agreement(self.permessage_deflate_offers)
153
+ if default_agreement is not None:
154
+ headers = headers + [(b'sec-websocket-extensions', default_agreement.as_header_value())]
155
+ response_headers = [(k, v) for k, v in headers if k not in {b'sec-websocket-extensions', b'sec-websocket-protocol'}]
156
+ agreement = negotiate_permessage_deflate(
157
+ request_headers=self.request.headers,
158
+ response_headers=headers,
159
+ )
160
+ if agreement is not None:
161
+ self.permessage_deflate_runtime = PerMessageDeflateRuntime(agreement)
162
+ response_headers.append((b'sec-websocket-extensions', agreement.as_header_value()))
163
+ if subprotocol is not None:
164
+ response_headers.append((b'sec-websocket-protocol', subprotocol.encode('ascii')))
165
+ await self.send_headers(200, response_headers, False)
166
+ self.accepted = True
167
+ self._record_activity()
168
+ if self.buffer or self.peer_end_stream_pending:
169
+ pending_end_stream = self.peer_end_stream_pending
170
+ self.peer_end_stream_pending = False
171
+ await self.feed_data(b'', end_stream=pending_end_stream)
172
+ return
173
+ if typ == 'websocket.send':
174
+ if not self.accepted:
175
+ raise RuntimeError('websocket.send before websocket.accept')
176
+ if self.closed:
177
+ return
178
+ text = message.get('text')
179
+ data = message.get('bytes')
180
+ if text is not None and data is not None:
181
+ raise RuntimeError('websocket.send cannot contain both text and bytes')
182
+ if text is not None:
183
+ payload = text.encode('utf-8')
184
+ if self.permessage_deflate_runtime is not None:
185
+ await self.send_data(serialize_frame(OP_TEXT, self.permessage_deflate_runtime.compress_message(payload), rsv1=True), False)
186
+ else:
187
+ await self.send_data(text_frame(text), False)
188
+ self._record_activity()
189
+ else:
190
+ raw = data or b''
191
+ if self.permessage_deflate_runtime is not None:
192
+ await self.send_data(binary_frame(self.permessage_deflate_runtime.compress_message(raw), rsv1=True), False)
193
+ else:
194
+ await self.send_data(binary_frame(raw), False)
195
+ self._record_activity()
196
+ return
197
+ if typ == 'websocket.close':
198
+ code = int(message.get('code', 1000))
199
+ reason = message.get('reason', '')
200
+ if not self.accepted:
201
+ self.http_denied = True
202
+ self.http_denial_status = 403
203
+ self.http_denial_headers = []
204
+ return
205
+ if not self.closed:
206
+ await self.send_data(close_frame(code, reason), True)
207
+ self.closed = True
208
+ self._notify_closed()
209
+ return
210
+ if typ == 'websocket.http.response.start':
211
+ if self.accepted:
212
+ raise RuntimeError('cannot deny websocket after accept')
213
+ self.http_denied = True
214
+ self.http_denial_status = int(message['status'])
215
+ self.http_denial_headers = list(message.get('headers', []))
216
+ return
217
+ if typ == 'websocket.http.response.body':
218
+ if not self.http_denied:
219
+ raise RuntimeError('websocket.http.response.body before denial start')
220
+ body = bytes(message.get('body', b''))
221
+ more = bool(message.get('more_body', False))
222
+ if not self.http_denial_started:
223
+ headers = list(self.http_denial_headers)
224
+ if not more:
225
+ headers.append((b'content-length', str(len(body)).encode('ascii')))
226
+ end_stream = (not body) and (not more)
227
+ await self.send_headers(self.http_denial_status, headers, end_stream)
228
+ self.http_denial_started = True
229
+ if end_stream:
230
+ self.closed = True
231
+ return
232
+ if body or not more:
233
+ await self.send_data(body, not more)
234
+ if not more:
235
+ self.closed = True
236
+ return
237
+ raise RuntimeError(f'unexpected websocket send message: {typ!r}')
238
+
239
+ def _frame_length(self, data: bytes) -> int | None:
240
+ if len(data) < 2:
241
+ return None
242
+ masked = bool(data[1] & 0x80)
243
+ length = data[1] & 0x7F
244
+ pos = 2
245
+ if length == 126:
246
+ if len(data) < pos + 2:
247
+ return None
248
+ length = int.from_bytes(data[pos:pos + 2], 'big')
249
+ pos += 2
250
+ elif length == 127:
251
+ if len(data) < pos + 8:
252
+ return None
253
+ length = int.from_bytes(data[pos:pos + 8], 'big')
254
+ pos += 8
255
+ if masked:
256
+ pos += 4
257
+ if len(data) < pos + length:
258
+ return None
259
+ return pos + length
260
+
261
+ def _inflate_if_needed(self, frame_payload: bytes, rsv1: bool) -> bytes:
262
+ if not rsv1:
263
+ return frame_payload
264
+ if self.permessage_deflate_runtime is None:
265
+ raise ProtocolError('RSV1 is not negotiated')
266
+ return self.permessage_deflate_runtime.decompress_message(frame_payload)
267
+
268
+ async def feed_data(self, data: bytes, *, end_stream: bool = False) -> None:
269
+ if self.closed:
270
+ return
271
+ self.buffer.extend(data)
272
+ if end_stream:
273
+ self.peer_end_stream_pending = True
274
+ if not self.accepted and not self.http_denied:
275
+ return
276
+ while self.buffer:
277
+ frame_len = self._frame_length(self.buffer)
278
+ if frame_len is None:
279
+ break
280
+ raw = bytes(self.buffer[:frame_len])
281
+ del self.buffer[:frame_len]
282
+ frame = parse_frame_bytes(
283
+ raw,
284
+ expect_masked=True,
285
+ max_payload_size=self.config.websocket_max_message_size,
286
+ allow_rsv1=self.permessage_deflate_runtime is not None,
287
+ )
288
+ self._record_activity()
289
+ if frame.opcode == OP_PING:
290
+ await self.send_data(pong_frame(frame.payload), False)
291
+ continue
292
+ if frame.opcode == OP_PONG:
293
+ if self.keepalive is not None:
294
+ self.keepalive.acknowledge_pong(frame.payload)
295
+ continue
296
+ if frame.opcode == OP_CLOSE:
297
+ code, reason = decode_close_payload(frame.payload)
298
+ if not self.closed:
299
+ await self.send_data(close_frame(code, reason), True)
300
+ self.closed = True
301
+ self._notify_closed()
302
+ await self.receive.put(websocket_disconnect(code, reason))
303
+ break
304
+ if frame.opcode in {OP_TEXT, OP_BINARY}:
305
+ if self.fragmented_opcode is not None:
306
+ raise ProtocolError('new data frame before fragmented message completion')
307
+ self.current_message_size = len(frame.payload)
308
+ if self.current_message_size > self.config.websocket_max_message_size:
309
+ raise ProtocolError('message too big')
310
+ if frame.fin:
311
+ payload = self._inflate_if_needed(frame.payload, frame.rsv1)
312
+ if frame.opcode == OP_TEXT:
313
+ await self.receive.put(websocket_receive_text(payload.decode('utf-8')))
314
+ else:
315
+ await self.receive.put(websocket_receive_bytes(payload))
316
+ self.current_message_size = 0
317
+ else:
318
+ self.fragmented_opcode = frame.opcode
319
+ self.fragmented_compressed = frame.rsv1
320
+ self.fragments = [frame.payload]
321
+ continue
322
+ if frame.opcode == OP_CONT:
323
+ if self.fragmented_opcode is None:
324
+ raise ProtocolError('unexpected continuation frame')
325
+ if frame.rsv1:
326
+ raise ProtocolError('RSV1 is only valid on the first frame of a compressed message')
327
+ self.current_message_size += len(frame.payload)
328
+ if self.current_message_size > self.config.websocket_max_message_size:
329
+ raise ProtocolError('message too big')
330
+ self.fragments.append(frame.payload)
331
+ if frame.fin:
332
+ message = b''.join(self.fragments)
333
+ if self.fragmented_compressed:
334
+ message = self._inflate_if_needed(message, True)
335
+ opcode = self.fragmented_opcode
336
+ self.fragmented_opcode = None
337
+ self.fragmented_compressed = False
338
+ self.fragments = []
339
+ self.current_message_size = 0
340
+ if opcode == OP_TEXT:
341
+ await self.receive.put(websocket_receive_text(message.decode('utf-8')))
342
+ else:
343
+ await self.receive.put(websocket_receive_bytes(message))
344
+ continue
345
+ raise ProtocolError('unsupported websocket opcode')
346
+ if self.peer_end_stream_pending and not self.closed:
347
+ self.peer_end_stream_pending = False
348
+ self.closed = True
349
+ self._notify_closed()
350
+ await self.receive.put(websocket_disconnect(1000, ''))
351
+
352
+ async def abort(self) -> None:
353
+ if not self.closed:
354
+ self.closed = True
355
+ self._notify_closed()
356
+ await self.receive.put(websocket_disconnect(1006, ''))
357
+ if self.task is not None:
358
+ self.task.cancel()
359
+ with suppress(asyncio.CancelledError):
360
+ await self.task
@@ -0,0 +1,82 @@
1
+ from .codec import (
2
+ H3_FRAME_UNEXPECTED,
3
+ H3_ID_ERROR,
4
+ H3_MESSAGE_ERROR,
5
+ H3_MISSING_SETTINGS,
6
+ H3_REQUEST_INCOMPLETE,
7
+ H3_SETTINGS_ERROR,
8
+ HTTP3ConnectionError,
9
+ HTTP3Frame,
10
+ HTTP3StreamError,
11
+ QPACK_DECODER_STREAM_ERROR,
12
+ QPACK_DECOMPRESSION_FAILED,
13
+ QPACK_ENCODER_STREAM_ERROR,
14
+ decode_frame,
15
+ decode_settings,
16
+ encode_frame,
17
+ encode_settings,
18
+ parse_frames,
19
+ )
20
+ from .qpack import (
21
+ FieldLine,
22
+ QpackBlocked,
23
+ QpackDecoder,
24
+ QpackDecoderStreamError,
25
+ QpackDecompressionFailed,
26
+ QpackEncoder,
27
+ QpackEncoderStreamError,
28
+ decode_field_section,
29
+ encode_field_section,
30
+ )
31
+ from .state import HTTP3BlockedSection, HTTP3ConnectionState, HTTP3PushPromiseState, HTTP3RequestState, HTTP3UniStreamState
32
+ from .streams import HTTP3ConnectionCore, HTTP3RequestStream
33
+
34
+ __all__ = [
35
+ 'HTTP3Frame',
36
+ 'FieldLine',
37
+ 'QpackBlocked',
38
+ 'QpackDecompressionFailed',
39
+ 'QpackEncoderStreamError',
40
+ 'QpackDecoderStreamError',
41
+ 'QpackDecoder',
42
+ 'QpackEncoder',
43
+ 'HTTP3ConnectionError',
44
+ 'HTTP3StreamError',
45
+ 'HTTP3BlockedSection',
46
+ 'HTTP3PushPromiseState',
47
+ 'HTTP3ConnectionState',
48
+ 'HTTP3RequestState',
49
+ 'HTTP3UniStreamState',
50
+ 'HTTP3ConnectionCore',
51
+ 'HTTP3RequestStream',
52
+ 'HTTP3DatagramHandler',
53
+ 'HTTP3Session',
54
+ 'encode_frame',
55
+ 'decode_frame',
56
+ 'parse_frames',
57
+ 'encode_settings',
58
+ 'decode_settings',
59
+ 'encode_field_section',
60
+ 'decode_field_section',
61
+ 'H3_FRAME_UNEXPECTED',
62
+ 'H3_ID_ERROR',
63
+ 'H3_MESSAGE_ERROR',
64
+ 'H3_MISSING_SETTINGS',
65
+ 'H3_REQUEST_INCOMPLETE',
66
+ 'H3_SETTINGS_ERROR',
67
+ 'QPACK_DECOMPRESSION_FAILED',
68
+ 'QPACK_ENCODER_STREAM_ERROR',
69
+ 'QPACK_DECODER_STREAM_ERROR',
70
+ ]
71
+
72
+
73
+ def __getattr__(name: str):
74
+ if name in {"HTTP3DatagramHandler", "HTTP3Session"}:
75
+ from .handler import HTTP3DatagramHandler, HTTP3Session
76
+
77
+ mapping = {
78
+ "HTTP3DatagramHandler": HTTP3DatagramHandler,
79
+ "HTTP3Session": HTTP3Session,
80
+ }
81
+ return mapping[name]
82
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,148 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Mapping
5
+
6
+ from tigrcorn_core.errors import ProtocolError
7
+ from tigrcorn_core.utils.bytes import decode_quic_varint, encode_quic_varint
8
+
9
+ FRAME_DATA = 0x0
10
+ FRAME_HEADERS = 0x1
11
+ FRAME_CANCEL_PUSH = 0x3
12
+ FRAME_SETTINGS = 0x4
13
+ FRAME_PUSH_PROMISE = 0x5
14
+ FRAME_GOAWAY = 0x7
15
+ FRAME_MAX_PUSH_ID = 0xD
16
+ STREAM_TYPE_CONTROL = 0x00
17
+ SETTING_ENABLE_CONNECT_PROTOCOL = 0x08
18
+ SETTING_H3_DATAGRAM = 0x33
19
+ SETTING_ENABLE_WEBTRANSPORT = 0x2B603742
20
+
21
+ H3_NO_ERROR = 0x0100
22
+ H3_GENERAL_PROTOCOL_ERROR = 0x0101
23
+ H3_INTERNAL_ERROR = 0x0102
24
+ H3_STREAM_CREATION_ERROR = 0x0103
25
+ H3_CLOSED_CRITICAL_STREAM = 0x0104
26
+ H3_FRAME_UNEXPECTED = 0x0105
27
+ H3_FRAME_ERROR = 0x0106
28
+ H3_EXCESSIVE_LOAD = 0x0107
29
+ H3_ID_ERROR = 0x0108
30
+ H3_SETTINGS_ERROR = 0x0109
31
+ H3_MISSING_SETTINGS = 0x010A
32
+ H3_REQUEST_REJECTED = 0x010B
33
+ H3_REQUEST_CANCELLED = 0x010C
34
+ H3_REQUEST_INCOMPLETE = 0x010D
35
+ H3_MESSAGE_ERROR = 0x010E
36
+ H3_CONNECT_ERROR = 0x010F
37
+ H3_VERSION_FALLBACK = 0x0110
38
+ QPACK_DECOMPRESSION_FAILED = 0x0200
39
+ QPACK_ENCODER_STREAM_ERROR = 0x0201
40
+ QPACK_DECODER_STREAM_ERROR = 0x0202
41
+
42
+ HTTP3_RESERVED_SETTINGS = frozenset({0x00, 0x02, 0x03, 0x04, 0x05})
43
+ HTTP3_RESERVED_FRAME_TYPES = frozenset({0x02, 0x06, 0x08, 0x09})
44
+
45
+
46
+ def is_reserved_setting(identifier: int) -> bool:
47
+ return identifier in HTTP3_RESERVED_SETTINGS
48
+
49
+
50
+
51
+ def is_reserved_frame_type(frame_type: int) -> bool:
52
+ return frame_type in HTTP3_RESERVED_FRAME_TYPES
53
+
54
+
55
+
56
+ def is_grease_identifier(identifier: int) -> bool:
57
+ return identifier >= 0x21 and (identifier - 0x21) % 0x1F == 0
58
+
59
+
60
+ class HTTP3Error(ProtocolError):
61
+ def __init__(self, message: str, *, error_code: int, stream_id: int | None = None) -> None:
62
+ super().__init__(message)
63
+ self.error_code = error_code
64
+ self.stream_id = stream_id
65
+
66
+
67
+ class HTTP3ConnectionError(HTTP3Error):
68
+ pass
69
+
70
+
71
+ class HTTP3StreamError(HTTP3Error):
72
+ pass
73
+
74
+
75
+ @dataclass(slots=True)
76
+ class HTTP3Frame:
77
+ frame_type: int
78
+ payload: bytes
79
+
80
+
81
+
82
+ def encode_frame(frame_type: int, payload: bytes = b'') -> bytes:
83
+ return encode_quic_varint(frame_type) + encode_quic_varint(len(payload)) + payload
84
+
85
+
86
+
87
+ def decode_frame(data: bytes, offset: int = 0) -> tuple[HTTP3Frame, int]:
88
+ frame_type, offset = decode_quic_varint(data, offset)
89
+ length, offset = decode_quic_varint(data, offset)
90
+ end = offset + length
91
+ if end > len(data):
92
+ raise ProtocolError('truncated HTTP/3 frame payload')
93
+ return HTTP3Frame(frame_type=frame_type, payload=data[offset:end]), end
94
+
95
+
96
+
97
+ def parse_frames(data: bytes) -> list[HTTP3Frame]:
98
+ frames: list[HTTP3Frame] = []
99
+ offset = 0
100
+ while offset < len(data):
101
+ frame, offset = decode_frame(data, offset)
102
+ frames.append(frame)
103
+ return frames
104
+
105
+
106
+
107
+ def encode_settings(settings: Mapping[int, int]) -> bytes:
108
+ payload = bytearray()
109
+ seen: set[int] = set()
110
+ for key, value in settings.items():
111
+ key_int = int(key)
112
+ if key_int in seen:
113
+ raise ProtocolError('duplicate HTTP/3 setting identifier')
114
+ if is_reserved_setting(key_int):
115
+ raise ProtocolError(f'reserved HTTP/3 setting identifier: {key_int:#x}')
116
+ seen.add(key_int)
117
+ payload.extend(encode_quic_varint(key_int))
118
+ payload.extend(encode_quic_varint(int(value)))
119
+ return bytes(payload)
120
+
121
+
122
+
123
+ def decode_settings(payload: bytes) -> dict[int, int]:
124
+ settings: dict[int, int] = {}
125
+ offset = 0
126
+ while offset < len(payload):
127
+ try:
128
+ key, offset = decode_quic_varint(payload, offset)
129
+ value, offset = decode_quic_varint(payload, offset)
130
+ except ProtocolError as exc:
131
+ raise HTTP3ConnectionError('malformed HTTP/3 SETTINGS payload', error_code=H3_SETTINGS_ERROR) from exc
132
+ if key in settings:
133
+ raise HTTP3ConnectionError('duplicate HTTP/3 setting', error_code=H3_SETTINGS_ERROR)
134
+ if is_reserved_setting(key):
135
+ raise HTTP3ConnectionError(f'reserved HTTP/3 setting received: {key:#x}', error_code=H3_SETTINGS_ERROR)
136
+ settings[key] = value
137
+ return settings
138
+
139
+
140
+
141
+ def decode_single_varint(payload: bytes, *, context: str) -> int:
142
+ try:
143
+ value, offset = decode_quic_varint(payload, 0)
144
+ except ProtocolError as exc:
145
+ raise HTTP3ConnectionError(f'malformed {context} frame payload', error_code=H3_FRAME_ERROR) from exc
146
+ if offset != len(payload):
147
+ raise HTTP3ConnectionError(f'invalid {context} frame size', error_code=H3_FRAME_ERROR)
148
+ return value
@@ -0,0 +1,3 @@
1
+ from .core import HTTP3DatagramHandler, HTTP3Session
2
+
3
+ __all__ = ["HTTP3DatagramHandler", "HTTP3Session"]