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,198 @@
1
+ from __future__ import annotations
2
+
3
+ from tigrcorn_core.utils.headers import apply_response_header_policy, get_header, sanitize_early_hints_headers, strip_connection_specific_headers
4
+
5
+
6
+ _REASON_PHRASES = {
7
+ 100: b"Continue",
8
+ 101: b"Switching Protocols",
9
+ 103: b"Early Hints",
10
+ 200: b"OK",
11
+ 201: b"Created",
12
+ 202: b"Accepted",
13
+ 204: b"No Content",
14
+ 206: b"Partial Content",
15
+ 301: b"Moved Permanently",
16
+ 302: b"Found",
17
+ 304: b"Not Modified",
18
+ 307: b"Temporary Redirect",
19
+ 308: b"Permanent Redirect",
20
+ 400: b"Bad Request",
21
+ 401: b"Unauthorized",
22
+ 402: b"Payment Required",
23
+ 403: b"Forbidden",
24
+ 404: b"Not Found",
25
+ 405: b"Method Not Allowed",
26
+ 406: b"Not Acceptable",
27
+ 408: b"Request Timeout",
28
+ 413: b"Content Too Large",
29
+ 416: b"Range Not Satisfiable",
30
+ 421: b"Misdirected Request",
31
+ 426: b"Upgrade Required",
32
+ 431: b"Request Header Fields Too Large",
33
+ 500: b"Internal Server Error",
34
+ 502: b"Bad Gateway",
35
+ 503: b"Service Unavailable",
36
+ 504: b"Gateway Timeout",
37
+ }
38
+
39
+
40
+ def _reason(status: int) -> bytes:
41
+ return _REASON_PHRASES.get(status, b"OK")
42
+
43
+
44
+
45
+ def response_allows_body(status: int) -> bool:
46
+ return not (100 <= status < 200 or status in {204, 304})
47
+
48
+
49
+
50
+ def response_allows_implicit_content_length(status: int) -> bool:
51
+ return response_allows_body(status)
52
+
53
+
54
+
55
+ def _normalize_response_headers(
56
+ *,
57
+ status: int,
58
+ headers: list[tuple[bytes, bytes]],
59
+ keep_alive: bool,
60
+ server_header: bytes | None,
61
+ chunked: bool,
62
+ include_date_header: bool,
63
+ default_headers: list[tuple[bytes, bytes]] | None,
64
+ alt_svc_values: list[bytes] | None,
65
+ ) -> list[tuple[bytes, bytes]]:
66
+ if 100 <= status < 200:
67
+ if status == 103:
68
+ return sanitize_early_hints_headers(headers)
69
+ return [(bytes(k).lower(), bytes(v)) for k, v in strip_connection_specific_headers(headers)]
70
+
71
+ normalized = apply_response_header_policy(
72
+ headers,
73
+ server_header=server_header,
74
+ include_date_header=include_date_header,
75
+ default_headers=default_headers or (),
76
+ alt_svc_values=alt_svc_values or (),
77
+ )
78
+ if get_header(normalized, b'connection') is None:
79
+ normalized.append((b'connection', b'keep-alive' if keep_alive else b'close'))
80
+
81
+ if not response_allows_body(status):
82
+ normalized = [(k, v) for k, v in normalized if k != b'transfer-encoding']
83
+ if status == 204:
84
+ normalized = [(k, v) for k, v in normalized if k != b'content-length']
85
+ return normalized
86
+
87
+ if chunked and get_header(normalized, b'transfer-encoding') is None and get_header(normalized, b'content-length') is None:
88
+ normalized.append((b'transfer-encoding', b'chunked'))
89
+ return normalized
90
+
91
+
92
+
93
+ HTTP11_RESPONSE_METADATA_RULES: tuple[dict[str, object], ...] = (
94
+ {
95
+ 'selector': '1xx',
96
+ 'allows_body': False,
97
+ 'allows_transfer_encoding': False,
98
+ 'allows_content_length': False,
99
+ 'implicit_content_length': False,
100
+ 'notes': 'informational responses never carry a final response body',
101
+ },
102
+ {
103
+ 'selector': '204',
104
+ 'allows_body': False,
105
+ 'allows_transfer_encoding': False,
106
+ 'allows_content_length': False,
107
+ 'implicit_content_length': False,
108
+ 'notes': '204 response body is always empty',
109
+ },
110
+ {
111
+ 'selector': '304',
112
+ 'allows_body': False,
113
+ 'allows_transfer_encoding': False,
114
+ 'allows_content_length': True,
115
+ 'implicit_content_length': False,
116
+ 'notes': '304 is bodyless but may carry representation metadata',
117
+ },
118
+ {
119
+ 'selector': 'other-final',
120
+ 'allows_body': True,
121
+ 'allows_transfer_encoding': True,
122
+ 'allows_content_length': True,
123
+ 'implicit_content_length': True,
124
+ 'notes': 'non-bodyless final responses may receive implicit Content-Length when fully buffered',
125
+ },
126
+ )
127
+
128
+
129
+ def http11_response_metadata_rules() -> tuple[dict[str, object], ...]:
130
+ return tuple(dict(entry) for entry in HTTP11_RESPONSE_METADATA_RULES)
131
+
132
+
133
+ def serialize_http11_response_head(
134
+ *,
135
+ status: int,
136
+ headers: list[tuple[bytes, bytes]],
137
+ keep_alive: bool,
138
+ server_header: bytes | None = None,
139
+ chunked: bool = False,
140
+ include_date_header: bool = True,
141
+ default_headers: list[tuple[bytes, bytes]] | None = None,
142
+ alt_svc_values: list[bytes] | None = None,
143
+ ) -> bytes:
144
+ normalized = _normalize_response_headers(
145
+ status=status,
146
+ headers=headers,
147
+ keep_alive=keep_alive,
148
+ server_header=server_header,
149
+ chunked=chunked,
150
+ include_date_header=include_date_header,
151
+ default_headers=default_headers,
152
+ alt_svc_values=alt_svc_values,
153
+ )
154
+ status_line = b"HTTP/1.1 " + str(status).encode("ascii") + b" " + _reason(status)
155
+ lines = [status_line] + [k + b": " + v for k, v in normalized]
156
+ return b"\r\n".join(lines) + b"\r\n\r\n"
157
+
158
+
159
+
160
+ def serialize_http11_response_whole(
161
+ *,
162
+ status: int,
163
+ headers: list[tuple[bytes, bytes]],
164
+ body: bytes,
165
+ keep_alive: bool,
166
+ server_header: bytes | None = None,
167
+ include_date_header: bool = True,
168
+ default_headers: list[tuple[bytes, bytes]] | None = None,
169
+ alt_svc_values: list[bytes] | None = None,
170
+ ) -> bytes:
171
+ normalized = [(k.lower(), v) for k, v in headers]
172
+ payload = body if response_allows_body(status) else b""
173
+ if response_allows_implicit_content_length(status) and get_header(normalized, b"content-length") is None:
174
+ normalized.append((b"content-length", str(len(payload)).encode("ascii")))
175
+ head = serialize_http11_response_head(
176
+ status=status,
177
+ headers=normalized,
178
+ keep_alive=keep_alive,
179
+ server_header=server_header,
180
+ chunked=False,
181
+ include_date_header=include_date_header,
182
+ default_headers=default_headers,
183
+ alt_svc_values=alt_svc_values,
184
+ )
185
+ return head + payload
186
+
187
+
188
+
189
+ def serialize_http11_response_chunk(chunk: bytes) -> bytes:
190
+ return f"{len(chunk):X}".encode("ascii") + b"\r\n" + chunk + b"\r\n"
191
+
192
+
193
+
194
+ def finalize_chunked_body(trailers: list[tuple[bytes, bytes]] | None = None) -> bytes:
195
+ if not trailers:
196
+ return b"0\r\n\r\n"
197
+ lines = [b"0"] + [bytes(name) + b": " + bytes(value) for name, value in trailers]
198
+ return b"\r\n".join(lines) + b"\r\n\r\n"
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(slots=True)
7
+ class HTTP11ConnectionState:
8
+ requests_served: int = 0
9
+ keep_alive: bool = True
@@ -0,0 +1,16 @@
1
+ from .codec import FrameBuffer, FrameWriter, HTTP2Frame
2
+ from .handler import HTTP2ConnectionHandler
3
+ from .hpack import decode_header_block, encode_header_block
4
+ from .state import H2ConnectionState, H2StreamLifecycle, H2StreamState
5
+
6
+ __all__ = [
7
+ "HTTP2Frame",
8
+ "FrameBuffer",
9
+ "FrameWriter",
10
+ "HTTP2ConnectionHandler",
11
+ "encode_header_block",
12
+ "decode_header_block",
13
+ "H2ConnectionState",
14
+ "H2StreamState",
15
+ "H2StreamLifecycle",
16
+ ]
@@ -0,0 +1,266 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Iterable, Mapping
5
+
6
+ from tigrcorn_core.errors import ProtocolError
7
+ from tigrcorn_core.utils.bytes import decode_u24, encode_u24, split_chunks
8
+
9
+ FRAME_DATA = 0x0
10
+ FRAME_HEADERS = 0x1
11
+ FRAME_PRIORITY = 0x2
12
+ FRAME_RST_STREAM = 0x3
13
+ FRAME_SETTINGS = 0x4
14
+ FRAME_PUSH_PROMISE = 0x5
15
+ FRAME_PING = 0x6
16
+ FRAME_GOAWAY = 0x7
17
+ FRAME_WINDOW_UPDATE = 0x8
18
+ FRAME_CONTINUATION = 0x9
19
+
20
+ H2_NO_ERROR = 0x0
21
+ H2_PROTOCOL_ERROR = 0x1
22
+ H2_INTERNAL_ERROR = 0x2
23
+ H2_FLOW_CONTROL_ERROR = 0x3
24
+ H2_SETTINGS_TIMEOUT = 0x4
25
+ H2_STREAM_CLOSED = 0x5
26
+ H2_FRAME_SIZE_ERROR = 0x6
27
+ H2_REFUSED_STREAM = 0x7
28
+ H2_CANCEL = 0x8
29
+ H2_COMPRESSION_ERROR = 0x9
30
+ H2_CONNECT_ERROR = 0xA
31
+ H2_ENHANCE_YOUR_CALM = 0xB
32
+ H2_INADEQUATE_SECURITY = 0xC
33
+ H2_HTTP_1_1_REQUIRED = 0xD
34
+
35
+ FLAG_ACK = 0x1
36
+ FLAG_END_STREAM = 0x1
37
+ FLAG_END_HEADERS = 0x4
38
+ FLAG_PADDED = 0x8
39
+ FLAG_PRIORITY = 0x20
40
+
41
+ SETTING_HEADER_TABLE_SIZE = 0x1
42
+ SETTING_ENABLE_PUSH = 0x2
43
+ SETTING_MAX_CONCURRENT_STREAMS = 0x3
44
+ SETTING_INITIAL_WINDOW_SIZE = 0x4
45
+ SETTING_MAX_FRAME_SIZE = 0x5
46
+ SETTING_MAX_HEADER_LIST_SIZE = 0x6
47
+ SETTING_ENABLE_CONNECT_PROTOCOL = 0x8
48
+
49
+ DEFAULT_SETTINGS = {
50
+ SETTING_HEADER_TABLE_SIZE: 4096,
51
+ SETTING_ENABLE_PUSH: 0,
52
+ SETTING_MAX_CONCURRENT_STREAMS: 128,
53
+ SETTING_INITIAL_WINDOW_SIZE: 65535,
54
+ SETTING_MAX_FRAME_SIZE: 16384,
55
+ SETTING_MAX_HEADER_LIST_SIZE: 65536,
56
+ SETTING_ENABLE_CONNECT_PROTOCOL: 1,
57
+ }
58
+
59
+
60
+ @dataclass(slots=True)
61
+ class HTTP2Frame:
62
+ frame_type: int
63
+ flags: int
64
+ stream_id: int
65
+ payload: bytes = b""
66
+
67
+ @property
68
+ def length(self) -> int:
69
+ return len(self.payload)
70
+
71
+
72
+ class FrameBuffer:
73
+ def __init__(self) -> None:
74
+ self._buffer = bytearray()
75
+
76
+ def feed(self, data: bytes) -> None:
77
+ self._buffer.extend(data)
78
+
79
+ def pop_all(self) -> list[HTTP2Frame]:
80
+ frames: list[HTTP2Frame] = []
81
+ while len(self._buffer) >= 9:
82
+ length = decode_u24(self._buffer[:3])
83
+ total = 9 + length
84
+ if len(self._buffer) < total:
85
+ break
86
+ frame_type = self._buffer[3]
87
+ flags = self._buffer[4]
88
+ stream_id = int.from_bytes(self._buffer[5:9], "big") & 0x7FFFFFFF
89
+ payload = bytes(self._buffer[9:total])
90
+ del self._buffer[:total]
91
+ frames.append(HTTP2Frame(frame_type=frame_type, flags=flags, stream_id=stream_id, payload=payload))
92
+ return frames
93
+
94
+
95
+ class FrameWriter:
96
+ def __init__(self, max_frame_size: int = 16384) -> None:
97
+ self.max_frame_size = max_frame_size
98
+
99
+ def headers(self, stream_id: int, block: bytes, *, end_stream: bool = False) -> bytes:
100
+ pieces = list(split_chunks(block, self.max_frame_size)) or [b""]
101
+ raw = bytearray()
102
+ for idx, piece in enumerate(pieces):
103
+ flags = 0
104
+ if idx == len(pieces) - 1:
105
+ flags |= FLAG_END_HEADERS
106
+ if end_stream:
107
+ flags |= FLAG_END_STREAM
108
+ raw.extend(serialize_frame(FRAME_HEADERS if idx == 0 else FRAME_CONTINUATION, flags, stream_id, piece))
109
+ return bytes(raw)
110
+
111
+ def push_promise(self, stream_id: int, promised_stream_id: int, block: bytes) -> bytes:
112
+ first_capacity = max(self.max_frame_size - 4, 0)
113
+ first_piece = block[:first_capacity]
114
+ remainder = block[first_capacity:]
115
+ payload = (promised_stream_id & 0x7FFFFFFF).to_bytes(4, "big") + first_piece
116
+ if not remainder:
117
+ return serialize_frame(FRAME_PUSH_PROMISE, FLAG_END_HEADERS, stream_id, payload)
118
+ raw = bytearray()
119
+ raw.extend(serialize_frame(FRAME_PUSH_PROMISE, 0, stream_id, payload))
120
+ pieces = list(split_chunks(remainder, self.max_frame_size))
121
+ for idx, piece in enumerate(pieces):
122
+ flags = FLAG_END_HEADERS if idx == len(pieces) - 1 else 0
123
+ raw.extend(serialize_frame(FRAME_CONTINUATION, flags, stream_id, piece))
124
+ return bytes(raw)
125
+
126
+ def data(self, stream_id: int, payload: bytes, *, end_stream: bool = False) -> bytes:
127
+ pieces = list(split_chunks(payload, self.max_frame_size)) or [b""]
128
+ raw = bytearray()
129
+ for idx, piece in enumerate(pieces):
130
+ flags = FLAG_END_STREAM if idx == len(pieces) - 1 and end_stream else 0
131
+ raw.extend(serialize_frame(FRAME_DATA, flags, stream_id, piece))
132
+ return bytes(raw)
133
+
134
+
135
+ def serialize_frame(frame_type: int, flags: int, stream_id: int, payload: bytes = b"") -> bytes:
136
+ if stream_id < 0 or stream_id > 0x7FFFFFFF:
137
+ raise ValueError("stream_id out of range")
138
+ header = bytearray()
139
+ header.extend(encode_u24(len(payload)))
140
+ header.append(frame_type & 0xFF)
141
+ header.append(flags & 0xFF)
142
+ header.extend((stream_id & 0x7FFFFFFF).to_bytes(4, "big"))
143
+ return bytes(header) + payload
144
+
145
+
146
+ def encode_settings(settings: Mapping[int, int]) -> bytes:
147
+ payload = bytearray()
148
+ for setting_id, value in settings.items():
149
+ payload.extend(int(setting_id).to_bytes(2, "big"))
150
+ payload.extend(int(value).to_bytes(4, "big"))
151
+ return bytes(payload)
152
+
153
+
154
+ def decode_settings(payload: bytes) -> dict[int, int]:
155
+ if len(payload) % 6 != 0:
156
+ raise ProtocolError("invalid SETTINGS payload length")
157
+ settings: dict[int, int] = {}
158
+ for offset in range(0, len(payload), 6):
159
+ key = int.from_bytes(payload[offset : offset + 2], "big")
160
+ value = int.from_bytes(payload[offset + 2 : offset + 6], "big")
161
+ if key in settings:
162
+ raise ProtocolError("duplicate SETTINGS parameter")
163
+ if key == SETTING_ENABLE_PUSH and value not in {0, 1}:
164
+ raise ProtocolError("ENABLE_PUSH must be 0 or 1")
165
+ if key == SETTING_INITIAL_WINDOW_SIZE and value > 0x7FFFFFFF:
166
+ raise ProtocolError("INITIAL_WINDOW_SIZE too large")
167
+ if key == SETTING_MAX_FRAME_SIZE and not 16_384 <= value <= 16_777_215:
168
+ raise ProtocolError("MAX_FRAME_SIZE out of range")
169
+ if key == SETTING_ENABLE_CONNECT_PROTOCOL and value not in {0, 1}:
170
+ raise ProtocolError("ENABLE_CONNECT_PROTOCOL must be 0 or 1")
171
+ settings[key] = value
172
+ return settings
173
+
174
+
175
+ def serialize_settings(settings: Mapping[int, int]) -> bytes:
176
+ return serialize_frame(FRAME_SETTINGS, 0, 0, encode_settings(settings))
177
+
178
+
179
+ def serialize_settings_ack() -> bytes:
180
+ return serialize_frame(FRAME_SETTINGS, FLAG_ACK, 0, b"")
181
+
182
+
183
+ def serialize_window_update(stream_id: int, increment: int) -> bytes:
184
+ if not 1 <= increment <= 0x7FFFFFFF:
185
+ raise ValueError("WINDOW_UPDATE increment out of range")
186
+ return serialize_frame(FRAME_WINDOW_UPDATE, 0, stream_id, increment.to_bytes(4, "big"))
187
+
188
+
189
+ def parse_window_update(payload: bytes) -> int:
190
+ if len(payload) != 4:
191
+ raise ProtocolError("WINDOW_UPDATE payload must be 4 bytes")
192
+ increment = int.from_bytes(payload, "big") & 0x7FFFFFFF
193
+ if increment <= 0:
194
+ raise ProtocolError("WINDOW_UPDATE increment must be positive")
195
+ return increment
196
+
197
+
198
+ def serialize_ping(data: bytes, *, ack: bool = False) -> bytes:
199
+ if len(data) != 8:
200
+ raise ValueError("PING payload must be 8 bytes")
201
+ return serialize_frame(FRAME_PING, FLAG_ACK if ack else 0, 0, data)
202
+
203
+
204
+ def serialize_goaway(last_stream_id: int, error_code: int = 0, debug_data: bytes = b"") -> bytes:
205
+ payload = bytearray()
206
+ payload.extend((last_stream_id & 0x7FFFFFFF).to_bytes(4, "big"))
207
+ payload.extend(int(error_code).to_bytes(4, "big"))
208
+ payload.extend(debug_data)
209
+ return serialize_frame(FRAME_GOAWAY, 0, 0, bytes(payload))
210
+
211
+
212
+ def parse_goaway(payload: bytes) -> tuple[int, int, bytes]:
213
+ if len(payload) < 8:
214
+ raise ProtocolError("GOAWAY payload too short")
215
+ last_stream_id = int.from_bytes(payload[:4], "big") & 0x7FFFFFFF
216
+ error_code = int.from_bytes(payload[4:8], "big")
217
+ return last_stream_id, error_code, payload[8:]
218
+
219
+
220
+ def parse_priority(payload: bytes) -> tuple[bool, int, int]:
221
+ if len(payload) != 5:
222
+ raise ProtocolError("PRIORITY payload must be 5 bytes")
223
+ dependency_raw = int.from_bytes(payload[:4], "big")
224
+ exclusive = bool(dependency_raw & 0x80000000)
225
+ dependency = dependency_raw & 0x7FFFFFFF
226
+ weight = payload[4]
227
+ return exclusive, dependency, weight
228
+
229
+
230
+ def parse_push_promise(payload: bytes, flags: int) -> tuple[int, bytes]:
231
+ payload = strip_padding(payload, flags)
232
+ if len(payload) < 4:
233
+ raise ProtocolError("PUSH_PROMISE payload too short")
234
+ promised_stream_id = int.from_bytes(payload[:4], "big") & 0x7FFFFFFF
235
+ return promised_stream_id, payload[4:]
236
+
237
+
238
+ def serialize_push_promise(stream_id: int, promised_stream_id: int, header_block_fragment: bytes, *, end_headers: bool = True) -> bytes:
239
+ flags = FLAG_END_HEADERS if end_headers else 0
240
+ payload = (promised_stream_id & 0x7FFFFFFF).to_bytes(4, "big") + header_block_fragment
241
+ return serialize_frame(FRAME_PUSH_PROMISE, flags, stream_id, payload)
242
+
243
+
244
+ def serialize_rst_stream(stream_id: int, error_code: int = 0) -> bytes:
245
+ return serialize_frame(FRAME_RST_STREAM, 0, stream_id, int(error_code).to_bytes(4, "big"))
246
+
247
+
248
+ def strip_padding(payload: bytes, flags: int) -> bytes:
249
+ if not (flags & FLAG_PADDED):
250
+ return payload
251
+ if not payload:
252
+ raise ProtocolError("PADDED frame missing pad length")
253
+ pad_length = payload[0]
254
+ body = payload[1:]
255
+ if pad_length > len(body):
256
+ raise ProtocolError("invalid padding")
257
+ return body[:-pad_length] if pad_length else body
258
+
259
+
260
+ def headers_payload_fragment(payload: bytes, flags: int) -> bytes:
261
+ payload = strip_padding(payload, flags)
262
+ if flags & FLAG_PRIORITY:
263
+ if len(payload) < 5:
264
+ raise ProtocolError("HEADERS priority payload too short")
265
+ payload = payload[5:]
266
+ return payload
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass, field
5
+
6
+ from tigrcorn_protocols.http2.state import FlowWindow, MAX_FLOW_WINDOW
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class FlowWaiter:
11
+ window: FlowWindow
12
+ _event: asyncio.Event = field(default_factory=asyncio.Event)
13
+
14
+ def __post_init__(self) -> None:
15
+ if self.window.available > 0:
16
+ self._event.set()
17
+
18
+ def notify(self) -> None:
19
+ if self.window.available > 0:
20
+ self._event.set()
21
+
22
+ async def wait(self) -> None:
23
+ while self.window.available <= 0:
24
+ self._event.clear()
25
+ await self._event.wait()
26
+
27
+
28
+ def next_adaptive_window_target(current_target: int, observed_bytes: int) -> int:
29
+ if observed_bytes <= 0:
30
+ return current_target
31
+ threshold = max(1, current_target // 2)
32
+ if observed_bytes < threshold:
33
+ return current_target
34
+ proposed = max(current_target * 2, observed_bytes * 2)
35
+ return min(MAX_FLOW_WINDOW, proposed)