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.
- tigrcorn_protocols/__init__.py +1 -0
- tigrcorn_protocols/_compression.py +219 -0
- tigrcorn_protocols/connect.py +107 -0
- tigrcorn_protocols/content_coding.py +179 -0
- tigrcorn_protocols/custom/__init__.py +3 -0
- tigrcorn_protocols/custom/adapters.py +18 -0
- tigrcorn_protocols/custom/registry.py +15 -0
- tigrcorn_protocols/flow/__init__.py +1 -0
- tigrcorn_protocols/flow/backpressure.py +17 -0
- tigrcorn_protocols/flow/buffers.py +29 -0
- tigrcorn_protocols/flow/credits.py +21 -0
- tigrcorn_protocols/flow/keepalive.py +85 -0
- tigrcorn_protocols/flow/timeouts.py +17 -0
- tigrcorn_protocols/flow/watermarks.py +16 -0
- tigrcorn_protocols/http1/__init__.py +16 -0
- tigrcorn_protocols/http1/keepalive.py +21 -0
- tigrcorn_protocols/http1/parser.py +481 -0
- tigrcorn_protocols/http1/serializer.py +198 -0
- tigrcorn_protocols/http1/state.py +9 -0
- tigrcorn_protocols/http2/__init__.py +16 -0
- tigrcorn_protocols/http2/codec.py +266 -0
- tigrcorn_protocols/http2/flow.py +35 -0
- tigrcorn_protocols/http2/handler.py +1303 -0
- tigrcorn_protocols/http2/hpack.py +393 -0
- tigrcorn_protocols/http2/state.py +226 -0
- tigrcorn_protocols/http2/streams.py +76 -0
- tigrcorn_protocols/http2/websocket.py +360 -0
- tigrcorn_protocols/http3/__init__.py +82 -0
- tigrcorn_protocols/http3/codec.py +148 -0
- tigrcorn_protocols/http3/handler/__init__.py +3 -0
- tigrcorn_protocols/http3/handler/core.py +1823 -0
- tigrcorn_protocols/http3/handler/webtransport.py +184 -0
- tigrcorn_protocols/http3/handler.py +3 -0
- tigrcorn_protocols/http3/qpack.py +843 -0
- tigrcorn_protocols/http3/state.py +129 -0
- tigrcorn_protocols/http3/streams.py +657 -0
- tigrcorn_protocols/http3/websocket.py +360 -0
- tigrcorn_protocols/lifespan/__init__.py +3 -0
- tigrcorn_protocols/lifespan/driver.py +83 -0
- tigrcorn_protocols/py.typed +1 -0
- tigrcorn_protocols/rawframed/__init__.py +5 -0
- tigrcorn_protocols/rawframed/codec.py +18 -0
- tigrcorn_protocols/rawframed/frames.py +28 -0
- tigrcorn_protocols/rawframed/handler.py +72 -0
- tigrcorn_protocols/rawframed/state.py +9 -0
- tigrcorn_protocols/registry.py +22 -0
- tigrcorn_protocols/scheduler/__init__.py +17 -0
- tigrcorn_protocols/scheduler/cancellation.py +40 -0
- tigrcorn_protocols/scheduler/dispatch.py +27 -0
- tigrcorn_protocols/scheduler/fairness.py +21 -0
- tigrcorn_protocols/scheduler/policy.py +12 -0
- tigrcorn_protocols/scheduler/priorities.py +8 -0
- tigrcorn_protocols/scheduler/quotas.py +19 -0
- tigrcorn_protocols/scheduler/runtime.py +156 -0
- tigrcorn_protocols/scheduler/tasks.py +31 -0
- tigrcorn_protocols/sessions/__init__.py +1 -0
- tigrcorn_protocols/sessions/base.py +16 -0
- tigrcorn_protocols/sessions/connection.py +12 -0
- tigrcorn_protocols/sessions/limits.py +12 -0
- tigrcorn_protocols/sessions/manager.py +31 -0
- tigrcorn_protocols/sessions/metadata.py +10 -0
- tigrcorn_protocols/sessions/quic.py +14 -0
- tigrcorn_protocols/streams/__init__.py +1 -0
- tigrcorn_protocols/streams/base.py +13 -0
- tigrcorn_protocols/streams/ids.py +5 -0
- tigrcorn_protocols/streams/multiplex.py +6 -0
- tigrcorn_protocols/streams/registry.py +22 -0
- tigrcorn_protocols/streams/singleplex.py +6 -0
- tigrcorn_protocols/websocket/__init__.py +1 -0
- tigrcorn_protocols/websocket/codec.py +31 -0
- tigrcorn_protocols/websocket/extensions.py +324 -0
- tigrcorn_protocols/websocket/frames.py +174 -0
- tigrcorn_protocols/websocket/handler.py +462 -0
- tigrcorn_protocols/websocket/handshake.py +66 -0
- tigrcorn_protocols/websocket/state.py +10 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/METADATA +240 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/RECORD +80 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/WHEEL +5 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/licenses/LICENSE +163 -0
- tigrcorn_protocols-0.3.16.dev5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from collections.abc import Callable, Iterable
|
|
5
|
+
|
|
6
|
+
from tigrcorn_core.errors import ProtocolError
|
|
7
|
+
from tigrcorn_protocols._compression import (
|
|
8
|
+
decode_prefixed_integer,
|
|
9
|
+
decode_prefixed_string,
|
|
10
|
+
encode_prefixed_integer,
|
|
11
|
+
encode_prefixed_string,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# RFC 9204 Appendix A static table (0-indexed).
|
|
15
|
+
_STATIC_TABLE: list[tuple[bytes, bytes]] = [
|
|
16
|
+
(b":authority", b""),
|
|
17
|
+
(b":path", b"/"),
|
|
18
|
+
(b"age", b"0"),
|
|
19
|
+
(b"content-disposition", b""),
|
|
20
|
+
(b"content-length", b"0"),
|
|
21
|
+
(b"cookie", b""),
|
|
22
|
+
(b"date", b""),
|
|
23
|
+
(b"etag", b""),
|
|
24
|
+
(b"if-modified-since", b""),
|
|
25
|
+
(b"if-none-match", b""),
|
|
26
|
+
(b"last-modified", b""),
|
|
27
|
+
(b"link", b""),
|
|
28
|
+
(b"location", b""),
|
|
29
|
+
(b"referer", b""),
|
|
30
|
+
(b"set-cookie", b""),
|
|
31
|
+
(b":method", b"CONNECT"),
|
|
32
|
+
(b":method", b"DELETE"),
|
|
33
|
+
(b":method", b"GET"),
|
|
34
|
+
(b":method", b"HEAD"),
|
|
35
|
+
(b":method", b"OPTIONS"),
|
|
36
|
+
(b":method", b"POST"),
|
|
37
|
+
(b":method", b"PUT"),
|
|
38
|
+
(b":scheme", b"http"),
|
|
39
|
+
(b":scheme", b"https"),
|
|
40
|
+
(b":status", b"103"),
|
|
41
|
+
(b":status", b"200"),
|
|
42
|
+
(b":status", b"304"),
|
|
43
|
+
(b":status", b"404"),
|
|
44
|
+
(b":status", b"503"),
|
|
45
|
+
(b"accept", b"*/*"),
|
|
46
|
+
(b"accept", b"application/dns-message"),
|
|
47
|
+
(b"accept-encoding", b"gzip, deflate, br"),
|
|
48
|
+
(b"accept-ranges", b"bytes"),
|
|
49
|
+
(b"access-control-allow-headers", b"cache-control"),
|
|
50
|
+
(b"access-control-allow-headers", b"content-type"),
|
|
51
|
+
(b"access-control-allow-origin", b"*"),
|
|
52
|
+
(b"cache-control", b"max-age=0"),
|
|
53
|
+
(b"cache-control", b"max-age=2592000"),
|
|
54
|
+
(b"cache-control", b"max-age=604800"),
|
|
55
|
+
(b"cache-control", b"no-cache"),
|
|
56
|
+
(b"cache-control", b"no-store"),
|
|
57
|
+
(b"cache-control", b"public, max-age=31536000"),
|
|
58
|
+
(b"content-encoding", b"br"),
|
|
59
|
+
(b"content-encoding", b"gzip"),
|
|
60
|
+
(b"content-type", b"application/dns-message"),
|
|
61
|
+
(b"content-type", b"application/javascript"),
|
|
62
|
+
(b"content-type", b"application/json"),
|
|
63
|
+
(b"content-type", b"application/x-www-form-urlencoded"),
|
|
64
|
+
(b"content-type", b"image/gif"),
|
|
65
|
+
(b"content-type", b"image/jpeg"),
|
|
66
|
+
(b"content-type", b"image/png"),
|
|
67
|
+
(b"content-type", b"text/css"),
|
|
68
|
+
(b"content-type", b"text/html; charset=utf-8"),
|
|
69
|
+
(b"content-type", b"text/plain"),
|
|
70
|
+
(b"content-type", b"text/plain;charset=utf-8"),
|
|
71
|
+
(b"range", b"bytes=0-"),
|
|
72
|
+
(b"strict-transport-security", b"max-age=31536000"),
|
|
73
|
+
(b"strict-transport-security", b"max-age=31536000; includesubdomains"),
|
|
74
|
+
(b"strict-transport-security", b"max-age=31536000; includesubdomains; preload"),
|
|
75
|
+
(b"vary", b"accept-encoding"),
|
|
76
|
+
(b"vary", b"origin"),
|
|
77
|
+
(b"x-content-type-options", b"nosniff"),
|
|
78
|
+
(b"x-xss-protection", b"1; mode=block"),
|
|
79
|
+
(b":status", b"100"),
|
|
80
|
+
(b":status", b"204"),
|
|
81
|
+
(b":status", b"206"),
|
|
82
|
+
(b":status", b"302"),
|
|
83
|
+
(b":status", b"400"),
|
|
84
|
+
(b":status", b"403"),
|
|
85
|
+
(b":status", b"421"),
|
|
86
|
+
(b":status", b"425"),
|
|
87
|
+
(b":status", b"500"),
|
|
88
|
+
(b"accept-language", b""),
|
|
89
|
+
(b"access-control-allow-credentials", b"FALSE"),
|
|
90
|
+
(b"access-control-allow-credentials", b"TRUE"),
|
|
91
|
+
(b"access-control-allow-headers", b"*"),
|
|
92
|
+
(b"access-control-allow-methods", b"get"),
|
|
93
|
+
(b"access-control-allow-methods", b"get, post, options"),
|
|
94
|
+
(b"access-control-allow-methods", b"options"),
|
|
95
|
+
(b"access-control-expose-headers", b"content-length"),
|
|
96
|
+
(b"access-control-request-headers", b"content-type"),
|
|
97
|
+
(b"access-control-request-method", b"get"),
|
|
98
|
+
(b"access-control-request-method", b"post"),
|
|
99
|
+
(b"alt-svc", b"clear"),
|
|
100
|
+
(b"authorization", b""),
|
|
101
|
+
(b"content-security-policy", b"script-src 'none'; object-src 'none'; base-uri 'none'"),
|
|
102
|
+
(b"early-data", b"1"),
|
|
103
|
+
(b"expect-ct", b""),
|
|
104
|
+
(b"forwarded", b""),
|
|
105
|
+
(b"if-range", b""),
|
|
106
|
+
(b"origin", b""),
|
|
107
|
+
(b"purpose", b"prefetch"),
|
|
108
|
+
(b"server", b""),
|
|
109
|
+
(b"timing-allow-origin", b"*"),
|
|
110
|
+
(b"upgrade-insecure-requests", b"1"),
|
|
111
|
+
(b"user-agent", b""),
|
|
112
|
+
(b"x-forwarded-for", b""),
|
|
113
|
+
(b"x-frame-options", b"deny"),
|
|
114
|
+
(b"x-frame-options", b"sameorigin"),
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
STATIC_INDEX: dict[tuple[bytes, bytes], int] = {entry: idx for idx, entry in enumerate(_STATIC_TABLE)}
|
|
118
|
+
STATIC_NAME_INDEX: dict[bytes, int] = {}
|
|
119
|
+
for idx, (name, _value) in enumerate(_STATIC_TABLE):
|
|
120
|
+
if name not in STATIC_NAME_INDEX:
|
|
121
|
+
STATIC_NAME_INDEX[name] = idx
|
|
122
|
+
|
|
123
|
+
SENSITIVE_HEADERS = {
|
|
124
|
+
b"authorization",
|
|
125
|
+
b"cookie",
|
|
126
|
+
b"proxy-authorization",
|
|
127
|
+
b"set-cookie",
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class QpackError(ProtocolError):
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class QpackBlocked(QpackError):
|
|
136
|
+
def __init__(self, required_insert_count: int) -> None:
|
|
137
|
+
super().__init__(f"QPACK field section is blocked on insert count {required_insert_count}")
|
|
138
|
+
self.required_insert_count = required_insert_count
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class QpackDecompressionFailed(QpackError):
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class QpackEncoderStreamError(QpackError):
|
|
146
|
+
pass
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class QpackDecoderStreamError(QpackError):
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(slots=True)
|
|
154
|
+
class FieldLine:
|
|
155
|
+
name: bytes
|
|
156
|
+
value: bytes
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass(slots=True)
|
|
160
|
+
class QpackFieldSection:
|
|
161
|
+
required_insert_count: int
|
|
162
|
+
base: int
|
|
163
|
+
headers: list[tuple[bytes, bytes]]
|
|
164
|
+
used_dynamic: bool = False
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass(slots=True)
|
|
168
|
+
class QpackDynamicEntry:
|
|
169
|
+
absolute_index: int
|
|
170
|
+
name: bytes
|
|
171
|
+
value: bytes
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def size(self) -> int:
|
|
175
|
+
return len(self.name) + len(self.value) + 32
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@dataclass(slots=True)
|
|
179
|
+
class _OutstandingSection:
|
|
180
|
+
required_insert_count: int
|
|
181
|
+
referenced_indexes: tuple[int, ...]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass(slots=True)
|
|
185
|
+
class _PlannedHeaderField:
|
|
186
|
+
kind: str
|
|
187
|
+
name: bytes
|
|
188
|
+
value: bytes
|
|
189
|
+
static_index: int | None = None
|
|
190
|
+
dynamic_absolute_index: int | None = None
|
|
191
|
+
|
|
192
|
+
def referenced_indexes(self) -> set[int]:
|
|
193
|
+
if self.dynamic_absolute_index is None:
|
|
194
|
+
return set()
|
|
195
|
+
return {self.dynamic_absolute_index}
|
|
196
|
+
|
|
197
|
+
def render(self, *, base: int, huffman: bool) -> bytes:
|
|
198
|
+
if self.kind == 'static_exact':
|
|
199
|
+
assert self.static_index is not None
|
|
200
|
+
return encode_qpack_integer(self.static_index, 6, 0xC0)
|
|
201
|
+
if self.kind == 'dynamic_exact':
|
|
202
|
+
assert self.dynamic_absolute_index is not None
|
|
203
|
+
relative_index = base - self.dynamic_absolute_index - 1
|
|
204
|
+
return encode_qpack_integer(relative_index, 6, 0x80)
|
|
205
|
+
if self.kind == 'static_name':
|
|
206
|
+
assert self.static_index is not None
|
|
207
|
+
return encode_qpack_integer(self.static_index, 4, 0x50) + encode_qpack_string(
|
|
208
|
+
self.value, 8, 0x00, huffman=huffman
|
|
209
|
+
)
|
|
210
|
+
if self.kind == 'dynamic_name':
|
|
211
|
+
assert self.dynamic_absolute_index is not None
|
|
212
|
+
relative_index = base - self.dynamic_absolute_index - 1
|
|
213
|
+
return encode_qpack_integer(relative_index, 4, 0x40) + encode_qpack_string(
|
|
214
|
+
self.value, 8, 0x00, huffman=huffman
|
|
215
|
+
)
|
|
216
|
+
if self.kind == 'literal':
|
|
217
|
+
return encode_qpack_string(self.name, 4, 0x20, huffman=huffman) + encode_qpack_string(
|
|
218
|
+
self.value, 8, 0x00, huffman=huffman
|
|
219
|
+
)
|
|
220
|
+
raise ProtocolError(f'unsupported QPACK header representation: {self.kind}')
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@dataclass(slots=True)
|
|
224
|
+
class QpackDynamicTable:
|
|
225
|
+
maximum_capacity: int = 0
|
|
226
|
+
capacity: int = 0
|
|
227
|
+
entries: list[QpackDynamicEntry] = field(default_factory=list) # newest first
|
|
228
|
+
size: int = 0
|
|
229
|
+
insert_count: int = 0
|
|
230
|
+
|
|
231
|
+
def max_entries(self) -> int:
|
|
232
|
+
return self.maximum_capacity // 32 if self.maximum_capacity > 0 else 0
|
|
233
|
+
|
|
234
|
+
def set_capacity(self, capacity: int, *, evictable: Callable[[QpackDynamicEntry], bool] | None = None) -> None:
|
|
235
|
+
if capacity < 0 or capacity > self.maximum_capacity:
|
|
236
|
+
raise ProtocolError('QPACK dynamic table capacity out of range')
|
|
237
|
+
self.capacity = capacity
|
|
238
|
+
if not self._evict_to_limit(0, evictable=evictable):
|
|
239
|
+
raise ProtocolError('QPACK dynamic table capacity would evict a referenced entry')
|
|
240
|
+
|
|
241
|
+
def _evict_to_limit(
|
|
242
|
+
self,
|
|
243
|
+
incoming_size: int,
|
|
244
|
+
*,
|
|
245
|
+
evictable: Callable[[QpackDynamicEntry], bool] | None = None,
|
|
246
|
+
) -> bool:
|
|
247
|
+
while self.size + incoming_size > self.capacity:
|
|
248
|
+
if not self.entries:
|
|
249
|
+
return False
|
|
250
|
+
evicted = self.entries[-1]
|
|
251
|
+
if evictable is not None and not evictable(evicted):
|
|
252
|
+
return False
|
|
253
|
+
self.entries.pop()
|
|
254
|
+
self.size -= evicted.size
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
def can_insert(
|
|
258
|
+
self,
|
|
259
|
+
name: bytes,
|
|
260
|
+
value: bytes,
|
|
261
|
+
*,
|
|
262
|
+
evictable: Callable[[QpackDynamicEntry], bool] | None = None,
|
|
263
|
+
) -> bool:
|
|
264
|
+
entry_size = len(name) + len(value) + 32
|
|
265
|
+
if entry_size > self.capacity:
|
|
266
|
+
return False
|
|
267
|
+
simulated_size = self.size
|
|
268
|
+
for entry in reversed(self.entries):
|
|
269
|
+
if simulated_size + entry_size <= self.capacity:
|
|
270
|
+
break
|
|
271
|
+
if evictable is not None and not evictable(entry):
|
|
272
|
+
return False
|
|
273
|
+
simulated_size -= entry.size
|
|
274
|
+
return simulated_size + entry_size <= self.capacity
|
|
275
|
+
|
|
276
|
+
def insert(
|
|
277
|
+
self,
|
|
278
|
+
name: bytes,
|
|
279
|
+
value: bytes,
|
|
280
|
+
*,
|
|
281
|
+
evictable: Callable[[QpackDynamicEntry], bool] | None = None,
|
|
282
|
+
) -> QpackDynamicEntry:
|
|
283
|
+
entry = QpackDynamicEntry(absolute_index=self.insert_count, name=name, value=value)
|
|
284
|
+
if entry.size > self.capacity:
|
|
285
|
+
raise ProtocolError('QPACK dynamic entry exceeds table capacity')
|
|
286
|
+
if not self._evict_to_limit(entry.size, evictable=evictable):
|
|
287
|
+
raise ProtocolError('QPACK dynamic entry would evict a referenced entry')
|
|
288
|
+
self.entries.insert(0, entry)
|
|
289
|
+
self.size += entry.size
|
|
290
|
+
self.insert_count += 1
|
|
291
|
+
return entry
|
|
292
|
+
|
|
293
|
+
def duplicate_relative(
|
|
294
|
+
self,
|
|
295
|
+
relative_index: int,
|
|
296
|
+
*,
|
|
297
|
+
evictable: Callable[[QpackDynamicEntry], bool] | None = None,
|
|
298
|
+
) -> QpackDynamicEntry:
|
|
299
|
+
entry = self.lookup_instruction_relative(relative_index)
|
|
300
|
+
return self.insert(entry.name, entry.value, evictable=evictable)
|
|
301
|
+
|
|
302
|
+
def lookup_static(self, index: int) -> tuple[bytes, bytes]:
|
|
303
|
+
if index < 0 or index >= len(_STATIC_TABLE):
|
|
304
|
+
raise ProtocolError(f'unsupported QPACK static index: {index}')
|
|
305
|
+
return _STATIC_TABLE[index]
|
|
306
|
+
|
|
307
|
+
def lookup_absolute_entry(self, absolute_index: int) -> QpackDynamicEntry:
|
|
308
|
+
for entry in self.entries:
|
|
309
|
+
if entry.absolute_index == absolute_index:
|
|
310
|
+
return entry
|
|
311
|
+
raise ProtocolError(f'unknown QPACK dynamic index: {absolute_index}')
|
|
312
|
+
|
|
313
|
+
def lookup_absolute(self, absolute_index: int) -> tuple[bytes, bytes]:
|
|
314
|
+
entry = self.lookup_absolute_entry(absolute_index)
|
|
315
|
+
return entry.name, entry.value
|
|
316
|
+
|
|
317
|
+
def absolute_index_from_relative(self, base: int, relative_index: int) -> int:
|
|
318
|
+
absolute_index = base - relative_index - 1
|
|
319
|
+
if absolute_index < 0:
|
|
320
|
+
raise ProtocolError('invalid QPACK relative index')
|
|
321
|
+
return absolute_index
|
|
322
|
+
|
|
323
|
+
def absolute_index_from_post_base(self, base: int, post_base_index: int) -> int:
|
|
324
|
+
absolute_index = base + post_base_index
|
|
325
|
+
if absolute_index < 0:
|
|
326
|
+
raise ProtocolError('invalid QPACK post-base index')
|
|
327
|
+
return absolute_index
|
|
328
|
+
|
|
329
|
+
def lookup_relative(self, base: int, relative_index: int) -> tuple[bytes, bytes]:
|
|
330
|
+
return self.lookup_absolute(self.absolute_index_from_relative(base, relative_index))
|
|
331
|
+
|
|
332
|
+
def lookup_post_base(self, base: int, post_base_index: int) -> tuple[bytes, bytes]:
|
|
333
|
+
return self.lookup_absolute(self.absolute_index_from_post_base(base, post_base_index))
|
|
334
|
+
|
|
335
|
+
def lookup_instruction_relative(self, relative_index: int) -> QpackDynamicEntry:
|
|
336
|
+
absolute_index = self.insert_count - relative_index - 1
|
|
337
|
+
if absolute_index < 0:
|
|
338
|
+
raise ProtocolError('invalid QPACK instruction relative index')
|
|
339
|
+
return self.lookup_absolute_entry(absolute_index)
|
|
340
|
+
|
|
341
|
+
def lookup_dynamic_exact(self, name: bytes, value: bytes, *, max_absolute_index: int | None = None) -> QpackDynamicEntry | None:
|
|
342
|
+
for entry in self.entries:
|
|
343
|
+
if max_absolute_index is not None and entry.absolute_index >= max_absolute_index:
|
|
344
|
+
continue
|
|
345
|
+
if entry.name == name and entry.value == value:
|
|
346
|
+
return entry
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
def lookup_dynamic_name(self, name: bytes, *, max_absolute_index: int | None = None) -> QpackDynamicEntry | None:
|
|
350
|
+
for entry in self.entries:
|
|
351
|
+
if max_absolute_index is not None and entry.absolute_index >= max_absolute_index:
|
|
352
|
+
continue
|
|
353
|
+
if entry.name == name:
|
|
354
|
+
return entry
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
# Wire helpers
|
|
359
|
+
|
|
360
|
+
def encode_qpack_integer(value: int, prefix_bits: int, prefix_mask: int = 0) -> bytes:
|
|
361
|
+
return encode_prefixed_integer(value, prefix_bits, prefix_mask)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def decode_qpack_integer(data: bytes, offset: int, prefix_bits: int) -> tuple[int, int]:
|
|
365
|
+
return decode_prefixed_integer(data, offset, prefix_bits)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def encode_qpack_string(data: bytes, prefix_bits: int = 8, prefix_mask: int = 0, *, huffman: bool = True) -> bytes:
|
|
369
|
+
return encode_prefixed_string(data, prefix_bits, prefix_mask, huffman=huffman)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def decode_qpack_string(data: bytes, offset: int, prefix_bits: int = 8) -> tuple[bytes, int]:
|
|
373
|
+
return decode_prefixed_string(data, offset, prefix_bits)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# Encoder stream instructions.
|
|
377
|
+
def encode_set_dynamic_table_capacity(capacity: int) -> bytes:
|
|
378
|
+
return encode_qpack_integer(capacity, 5, 0x20)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def encode_insert_with_name_reference(name_index: int, value: bytes, *, static: bool, huffman: bool = True) -> bytes:
|
|
382
|
+
return encode_qpack_integer(name_index, 6, 0xC0 if static else 0x80) + encode_qpack_string(
|
|
383
|
+
value, 8, 0x00, huffman=huffman
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def encode_insert_with_literal_name(name: bytes, value: bytes, *, huffman: bool = True) -> bytes:
|
|
388
|
+
return encode_qpack_string(name, 6, 0x40, huffman=huffman) + encode_qpack_string(value, 8, 0x00, huffman=huffman)
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def encode_duplicate(relative_index: int) -> bytes:
|
|
392
|
+
return encode_qpack_integer(relative_index, 5, 0x00)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# Decoder stream instructions.
|
|
396
|
+
def encode_section_ack(stream_id: int) -> bytes:
|
|
397
|
+
return encode_qpack_integer(stream_id, 7, 0x80)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def encode_stream_cancellation(stream_id: int) -> bytes:
|
|
401
|
+
return encode_qpack_integer(stream_id, 6, 0x40)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def encode_insert_count_increment(increment: int) -> bytes:
|
|
405
|
+
if increment <= 0:
|
|
406
|
+
raise ProtocolError('QPACK insert count increment must be positive')
|
|
407
|
+
return encode_qpack_integer(increment, 6, 0x00)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
class QpackEncoder:
|
|
411
|
+
def __init__(
|
|
412
|
+
self,
|
|
413
|
+
*,
|
|
414
|
+
max_table_capacity: int = 0,
|
|
415
|
+
blocked_streams: int = 0,
|
|
416
|
+
use_huffman: bool = True,
|
|
417
|
+
sensitive_headers: set[bytes] | None = None,
|
|
418
|
+
) -> None:
|
|
419
|
+
self.dynamic_table = QpackDynamicTable(maximum_capacity=max_table_capacity, capacity=0)
|
|
420
|
+
self.blocked_streams = blocked_streams
|
|
421
|
+
self.use_huffman = use_huffman
|
|
422
|
+
self.sensitive_headers = set(SENSITIVE_HEADERS if sensitive_headers is None else sensitive_headers)
|
|
423
|
+
self.known_received_count = 0
|
|
424
|
+
self._pending_encoder_bytes = bytearray()
|
|
425
|
+
self._announced_capacity = 0
|
|
426
|
+
self._outstanding_sections: dict[int, list[_OutstandingSection]] = {}
|
|
427
|
+
self._reference_counts: dict[int, int] = {}
|
|
428
|
+
|
|
429
|
+
def _evictable_entry(self, entry: QpackDynamicEntry) -> bool:
|
|
430
|
+
return entry.absolute_index < self.known_received_count and self._reference_counts.get(entry.absolute_index, 0) == 0
|
|
431
|
+
|
|
432
|
+
def _ensure_capacity_announced(self) -> None:
|
|
433
|
+
target = self.dynamic_table.maximum_capacity
|
|
434
|
+
if target > 0 and self._announced_capacity != target:
|
|
435
|
+
self.dynamic_table.set_capacity(target, evictable=self._evictable_entry)
|
|
436
|
+
self._pending_encoder_bytes.extend(encode_set_dynamic_table_capacity(target))
|
|
437
|
+
self._announced_capacity = target
|
|
438
|
+
|
|
439
|
+
def _should_index(self, name: bytes, value: bytes) -> bool:
|
|
440
|
+
if self.dynamic_table.maximum_capacity <= 0 or name in self.sensitive_headers:
|
|
441
|
+
return False
|
|
442
|
+
return self.dynamic_table.can_insert(name, value, evictable=self._evictable_entry)
|
|
443
|
+
|
|
444
|
+
def _queue_insert(self, name: bytes, value: bytes) -> QpackDynamicEntry:
|
|
445
|
+
static_name_index = STATIC_NAME_INDEX.get(name)
|
|
446
|
+
dynamic_name_entry = self.dynamic_table.lookup_dynamic_name(name)
|
|
447
|
+
if static_name_index is not None:
|
|
448
|
+
self._pending_encoder_bytes.extend(
|
|
449
|
+
encode_insert_with_name_reference(static_name_index, value, static=True, huffman=self.use_huffman)
|
|
450
|
+
)
|
|
451
|
+
elif dynamic_name_entry is not None:
|
|
452
|
+
relative_index = self.dynamic_table.insert_count - dynamic_name_entry.absolute_index - 1
|
|
453
|
+
self._pending_encoder_bytes.extend(
|
|
454
|
+
encode_insert_with_name_reference(relative_index, value, static=False, huffman=self.use_huffman)
|
|
455
|
+
)
|
|
456
|
+
else:
|
|
457
|
+
self._pending_encoder_bytes.extend(encode_insert_with_literal_name(name, value, huffman=self.use_huffman))
|
|
458
|
+
return self.dynamic_table.insert(name, value, evictable=self._evictable_entry)
|
|
459
|
+
|
|
460
|
+
def _encode_prefix(self, required_insert_count: int, base: int) -> bytes:
|
|
461
|
+
max_entries = self.dynamic_table.max_entries()
|
|
462
|
+
if required_insert_count == 0:
|
|
463
|
+
encoded_required = 0
|
|
464
|
+
else:
|
|
465
|
+
if max_entries <= 0:
|
|
466
|
+
raise ProtocolError('QPACK dynamic references require non-zero table capacity')
|
|
467
|
+
encoded_required = (required_insert_count % (2 * max_entries)) + 1
|
|
468
|
+
if base < required_insert_count:
|
|
469
|
+
sign = 1
|
|
470
|
+
delta = required_insert_count - base - 1
|
|
471
|
+
else:
|
|
472
|
+
sign = 0
|
|
473
|
+
delta = base - required_insert_count
|
|
474
|
+
return encode_qpack_integer(encoded_required, 8, 0x00) + encode_qpack_integer(delta, 7, 0x80 if sign else 0x00)
|
|
475
|
+
|
|
476
|
+
def _blocked_stream_ids(self) -> set[int]:
|
|
477
|
+
blocked: set[int] = set()
|
|
478
|
+
for stream_id, sections in self._outstanding_sections.items():
|
|
479
|
+
if any(section.required_insert_count > self.known_received_count for section in sections):
|
|
480
|
+
blocked.add(stream_id)
|
|
481
|
+
return blocked
|
|
482
|
+
|
|
483
|
+
def _can_risk_blocking(self, stream_id: int) -> bool:
|
|
484
|
+
if self.blocked_streams <= 0:
|
|
485
|
+
return False
|
|
486
|
+
blocked_stream_ids = self._blocked_stream_ids()
|
|
487
|
+
return stream_id in blocked_stream_ids or len(blocked_stream_ids) < self.blocked_streams
|
|
488
|
+
|
|
489
|
+
def _plan_header(self, name: bytes, value: bytes, *, reference_limit: int) -> _PlannedHeaderField:
|
|
490
|
+
static_exact = STATIC_INDEX.get((name, value))
|
|
491
|
+
if static_exact is not None:
|
|
492
|
+
return _PlannedHeaderField(kind='static_exact', name=name, value=value, static_index=static_exact)
|
|
493
|
+
dynamic_exact = self.dynamic_table.lookup_dynamic_exact(name, value, max_absolute_index=reference_limit)
|
|
494
|
+
if dynamic_exact is not None:
|
|
495
|
+
return _PlannedHeaderField(
|
|
496
|
+
kind='dynamic_exact',
|
|
497
|
+
name=name,
|
|
498
|
+
value=value,
|
|
499
|
+
dynamic_absolute_index=dynamic_exact.absolute_index,
|
|
500
|
+
)
|
|
501
|
+
static_name = STATIC_NAME_INDEX.get(name)
|
|
502
|
+
if static_name is not None:
|
|
503
|
+
return _PlannedHeaderField(kind='static_name', name=name, value=value, static_index=static_name)
|
|
504
|
+
dynamic_name = self.dynamic_table.lookup_dynamic_name(name, max_absolute_index=reference_limit)
|
|
505
|
+
if dynamic_name is not None:
|
|
506
|
+
return _PlannedHeaderField(
|
|
507
|
+
kind='dynamic_name',
|
|
508
|
+
name=name,
|
|
509
|
+
value=value,
|
|
510
|
+
dynamic_absolute_index=dynamic_name.absolute_index,
|
|
511
|
+
)
|
|
512
|
+
return _PlannedHeaderField(kind='literal', name=name, value=value)
|
|
513
|
+
|
|
514
|
+
def _track_outstanding_section(self, stream_id: int, *, required_insert_count: int, referenced_indexes: set[int]) -> None:
|
|
515
|
+
if required_insert_count <= 0:
|
|
516
|
+
return
|
|
517
|
+
ordered_indexes = tuple(sorted(referenced_indexes))
|
|
518
|
+
self._outstanding_sections.setdefault(stream_id, []).append(
|
|
519
|
+
_OutstandingSection(required_insert_count=required_insert_count, referenced_indexes=ordered_indexes)
|
|
520
|
+
)
|
|
521
|
+
for absolute_index in ordered_indexes:
|
|
522
|
+
self._reference_counts[absolute_index] = self._reference_counts.get(absolute_index, 0) + 1
|
|
523
|
+
|
|
524
|
+
def _release_section(self, section: _OutstandingSection) -> None:
|
|
525
|
+
for absolute_index in section.referenced_indexes:
|
|
526
|
+
remaining = self._reference_counts.get(absolute_index, 0) - 1
|
|
527
|
+
if remaining > 0:
|
|
528
|
+
self._reference_counts[absolute_index] = remaining
|
|
529
|
+
else:
|
|
530
|
+
self._reference_counts.pop(absolute_index, None)
|
|
531
|
+
|
|
532
|
+
def encode_field_section(self, headers: Iterable[tuple[bytes, bytes]], *, stream_id: int = 0) -> bytes:
|
|
533
|
+
header_list = [(bytes(name), bytes(value)) for name, value in headers]
|
|
534
|
+
allow_blocking = self._can_risk_blocking(stream_id)
|
|
535
|
+
if self.dynamic_table.maximum_capacity > 0:
|
|
536
|
+
self._ensure_capacity_announced()
|
|
537
|
+
if allow_blocking:
|
|
538
|
+
inserted: set[tuple[bytes, bytes]] = set()
|
|
539
|
+
for name, value in header_list:
|
|
540
|
+
if not self._should_index(name, value):
|
|
541
|
+
continue
|
|
542
|
+
if STATIC_INDEX.get((name, value)) is not None:
|
|
543
|
+
continue
|
|
544
|
+
if self.dynamic_table.lookup_dynamic_exact(name, value) is not None:
|
|
545
|
+
continue
|
|
546
|
+
candidate = (name, value)
|
|
547
|
+
if candidate in inserted:
|
|
548
|
+
continue
|
|
549
|
+
try:
|
|
550
|
+
self._queue_insert(name, value)
|
|
551
|
+
except ProtocolError:
|
|
552
|
+
continue
|
|
553
|
+
inserted.add(candidate)
|
|
554
|
+
reference_limit = self.dynamic_table.insert_count if allow_blocking else self.known_received_count
|
|
555
|
+
plans = [self._plan_header(name, value, reference_limit=reference_limit) for name, value in header_list]
|
|
556
|
+
referenced_indexes: set[int] = set()
|
|
557
|
+
for plan in plans:
|
|
558
|
+
referenced_indexes.update(plan.referenced_indexes())
|
|
559
|
+
required_insert_count = max((absolute_index + 1 for absolute_index in referenced_indexes), default=0)
|
|
560
|
+
base = required_insert_count
|
|
561
|
+
encoded = bytearray(self._encode_prefix(required_insert_count, base))
|
|
562
|
+
for plan in plans:
|
|
563
|
+
encoded.extend(plan.render(base=base, huffman=self.use_huffman))
|
|
564
|
+
self._track_outstanding_section(stream_id, required_insert_count=required_insert_count, referenced_indexes=referenced_indexes)
|
|
565
|
+
return bytes(encoded)
|
|
566
|
+
|
|
567
|
+
def receive_decoder_stream(self, data: bytes) -> None:
|
|
568
|
+
offset = 0
|
|
569
|
+
while offset < len(data):
|
|
570
|
+
first = data[offset]
|
|
571
|
+
if first & 0x80:
|
|
572
|
+
stream_id, offset = decode_qpack_integer(data, offset, 7)
|
|
573
|
+
outstanding = self._outstanding_sections.get(stream_id)
|
|
574
|
+
if not outstanding:
|
|
575
|
+
raise QpackDecoderStreamError('unexpected QPACK section acknowledgment')
|
|
576
|
+
section = outstanding.pop(0)
|
|
577
|
+
self._release_section(section)
|
|
578
|
+
self.known_received_count = max(self.known_received_count, section.required_insert_count)
|
|
579
|
+
if not outstanding:
|
|
580
|
+
self._outstanding_sections.pop(stream_id, None)
|
|
581
|
+
continue
|
|
582
|
+
if first & 0x40:
|
|
583
|
+
stream_id, offset = decode_qpack_integer(data, offset, 6)
|
|
584
|
+
cancelled = self._outstanding_sections.pop(stream_id, [])
|
|
585
|
+
for section in cancelled:
|
|
586
|
+
self._release_section(section)
|
|
587
|
+
continue
|
|
588
|
+
increment, offset = decode_qpack_integer(data, offset, 6)
|
|
589
|
+
if increment <= 0:
|
|
590
|
+
raise QpackDecoderStreamError('invalid QPACK insert count increment')
|
|
591
|
+
if self.known_received_count + increment > self.dynamic_table.insert_count:
|
|
592
|
+
raise QpackDecoderStreamError('QPACK insert count increment exceeds sent inserts')
|
|
593
|
+
self.known_received_count += increment
|
|
594
|
+
|
|
595
|
+
def take_encoder_stream_data(self) -> bytes:
|
|
596
|
+
payload = bytes(self._pending_encoder_bytes)
|
|
597
|
+
self._pending_encoder_bytes.clear()
|
|
598
|
+
return payload
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class QpackDecoder:
|
|
602
|
+
def __init__(self, *, max_table_capacity: int = 0, blocked_streams: int = 0) -> None:
|
|
603
|
+
self.dynamic_table = QpackDynamicTable(maximum_capacity=max_table_capacity, capacity=0)
|
|
604
|
+
self.blocked_streams = blocked_streams
|
|
605
|
+
self.known_received_count = 0
|
|
606
|
+
self._pending_decoder_bytes = bytearray()
|
|
607
|
+
self._blocked_requirements: dict[int, list[int]] = {}
|
|
608
|
+
|
|
609
|
+
def _decode_required_insert_count(self, encoded_required: int) -> int:
|
|
610
|
+
max_entries = self.dynamic_table.max_entries()
|
|
611
|
+
if encoded_required == 0:
|
|
612
|
+
return 0
|
|
613
|
+
if max_entries <= 0:
|
|
614
|
+
raise QpackDecompressionFailed('QPACK dynamic references require non-zero table capacity')
|
|
615
|
+
full_range = 2 * max_entries
|
|
616
|
+
if encoded_required > full_range:
|
|
617
|
+
raise QpackDecompressionFailed('invalid QPACK encoded required insert count')
|
|
618
|
+
max_value = self.dynamic_table.insert_count + max_entries
|
|
619
|
+
max_wrapped = (max_value // full_range) * full_range
|
|
620
|
+
required = max_wrapped + encoded_required - 1
|
|
621
|
+
if required > max_value:
|
|
622
|
+
if required <= full_range:
|
|
623
|
+
raise QpackDecompressionFailed('invalid QPACK required insert count')
|
|
624
|
+
required -= full_range
|
|
625
|
+
if required == 0:
|
|
626
|
+
raise QpackDecompressionFailed('QPACK zero required insert count must be encoded as zero')
|
|
627
|
+
return required
|
|
628
|
+
|
|
629
|
+
def _mark_blocked(self, stream_id: int | None, required_insert_count: int) -> None:
|
|
630
|
+
if stream_id is None:
|
|
631
|
+
return
|
|
632
|
+
blocked = self._blocked_requirements.get(stream_id)
|
|
633
|
+
if blocked is None:
|
|
634
|
+
if len(self._blocked_requirements) >= self.blocked_streams:
|
|
635
|
+
raise QpackDecompressionFailed('QPACK blocked streams limit exceeded')
|
|
636
|
+
blocked = []
|
|
637
|
+
self._blocked_requirements[stream_id] = blocked
|
|
638
|
+
blocked.append(required_insert_count)
|
|
639
|
+
|
|
640
|
+
def _unmark_blocked(self, stream_id: int | None, required_insert_count: int) -> None:
|
|
641
|
+
if stream_id is None:
|
|
642
|
+
return
|
|
643
|
+
blocked = self._blocked_requirements.get(stream_id)
|
|
644
|
+
if not blocked:
|
|
645
|
+
return
|
|
646
|
+
try:
|
|
647
|
+
blocked.remove(required_insert_count)
|
|
648
|
+
except ValueError:
|
|
649
|
+
return
|
|
650
|
+
if not blocked:
|
|
651
|
+
self._blocked_requirements.pop(stream_id, None)
|
|
652
|
+
|
|
653
|
+
def _lookup_encoder_stream_name(self, *, static: bool, name_index: int) -> bytes:
|
|
654
|
+
try:
|
|
655
|
+
if static:
|
|
656
|
+
name, _value = self.dynamic_table.lookup_static(name_index)
|
|
657
|
+
return name
|
|
658
|
+
entry = self.dynamic_table.lookup_instruction_relative(name_index)
|
|
659
|
+
return entry.name
|
|
660
|
+
except ProtocolError as exc:
|
|
661
|
+
raise QpackEncoderStreamError('invalid QPACK encoder stream name reference') from exc
|
|
662
|
+
|
|
663
|
+
def _require_dynamic_entry(self, absolute_index: int, *, required_insert_count: int) -> tuple[bytes, bytes]:
|
|
664
|
+
if required_insert_count <= 0 or absolute_index >= required_insert_count:
|
|
665
|
+
raise QpackDecompressionFailed('invalid QPACK dynamic table reference')
|
|
666
|
+
try:
|
|
667
|
+
return self.dynamic_table.lookup_absolute(absolute_index)
|
|
668
|
+
except ProtocolError as exc:
|
|
669
|
+
raise QpackDecompressionFailed('invalid QPACK dynamic table reference') from exc
|
|
670
|
+
|
|
671
|
+
def _resolve_name(self, *, static: bool, base: int, index: int, post_base: bool = False, required_insert_count: int) -> bytes:
|
|
672
|
+
if static:
|
|
673
|
+
try:
|
|
674
|
+
name, _value = self.dynamic_table.lookup_static(index)
|
|
675
|
+
except ProtocolError as exc:
|
|
676
|
+
raise QpackDecompressionFailed('invalid QPACK static table index') from exc
|
|
677
|
+
return name
|
|
678
|
+
try:
|
|
679
|
+
absolute_index = (
|
|
680
|
+
self.dynamic_table.absolute_index_from_post_base(base, index)
|
|
681
|
+
if post_base
|
|
682
|
+
else self.dynamic_table.absolute_index_from_relative(base, index)
|
|
683
|
+
)
|
|
684
|
+
except ProtocolError as exc:
|
|
685
|
+
raise QpackDecompressionFailed('invalid QPACK dynamic name reference') from exc
|
|
686
|
+
name, _value = self._require_dynamic_entry(absolute_index, required_insert_count=required_insert_count)
|
|
687
|
+
return name
|
|
688
|
+
|
|
689
|
+
def receive_encoder_stream(self, data: bytes) -> None:
|
|
690
|
+
offset = 0
|
|
691
|
+
processed_inserts = 0
|
|
692
|
+
while offset < len(data):
|
|
693
|
+
first = data[offset]
|
|
694
|
+
if first & 0x80:
|
|
695
|
+
static = bool(first & 0x40)
|
|
696
|
+
name_index, offset = decode_qpack_integer(data, offset, 6)
|
|
697
|
+
name = self._lookup_encoder_stream_name(static=static, name_index=name_index)
|
|
698
|
+
try:
|
|
699
|
+
value, offset = decode_qpack_string(data, offset, 8)
|
|
700
|
+
self.dynamic_table.insert(name, value)
|
|
701
|
+
except ProtocolError as exc:
|
|
702
|
+
raise QpackEncoderStreamError('invalid QPACK encoder stream insertion') from exc
|
|
703
|
+
processed_inserts += 1
|
|
704
|
+
continue
|
|
705
|
+
if first & 0x40:
|
|
706
|
+
try:
|
|
707
|
+
name, offset = decode_qpack_string(data, offset, 6)
|
|
708
|
+
value, offset = decode_qpack_string(data, offset, 8)
|
|
709
|
+
self.dynamic_table.insert(name, value)
|
|
710
|
+
except ProtocolError as exc:
|
|
711
|
+
raise QpackEncoderStreamError('invalid QPACK encoder stream literal insertion') from exc
|
|
712
|
+
processed_inserts += 1
|
|
713
|
+
continue
|
|
714
|
+
if first & 0x20:
|
|
715
|
+
try:
|
|
716
|
+
capacity, offset = decode_qpack_integer(data, offset, 5)
|
|
717
|
+
self.dynamic_table.set_capacity(capacity)
|
|
718
|
+
except ProtocolError as exc:
|
|
719
|
+
raise QpackEncoderStreamError('invalid QPACK encoder stream capacity update') from exc
|
|
720
|
+
continue
|
|
721
|
+
try:
|
|
722
|
+
relative_index, offset = decode_qpack_integer(data, offset, 5)
|
|
723
|
+
self.dynamic_table.duplicate_relative(relative_index)
|
|
724
|
+
except ProtocolError as exc:
|
|
725
|
+
raise QpackEncoderStreamError('invalid QPACK duplicate instruction') from exc
|
|
726
|
+
processed_inserts += 1
|
|
727
|
+
if processed_inserts:
|
|
728
|
+
self.known_received_count += processed_inserts
|
|
729
|
+
self._pending_decoder_bytes.extend(encode_insert_count_increment(processed_inserts))
|
|
730
|
+
|
|
731
|
+
def decode_field_section(self, data: bytes, *, stream_id: int | None = 0) -> QpackFieldSection:
|
|
732
|
+
offset = 0
|
|
733
|
+
encoded_required, offset = decode_qpack_integer(data, offset, 8)
|
|
734
|
+
required_insert_count = self._decode_required_insert_count(encoded_required)
|
|
735
|
+
if required_insert_count > self.dynamic_table.insert_count:
|
|
736
|
+
self._mark_blocked(stream_id, required_insert_count)
|
|
737
|
+
raise QpackBlocked(required_insert_count)
|
|
738
|
+
if offset >= len(data):
|
|
739
|
+
raise QpackDecompressionFailed('truncated QPACK field section prefix')
|
|
740
|
+
sign = bool(data[offset] & 0x80)
|
|
741
|
+
delta_base, offset = decode_qpack_integer(data, offset, 7)
|
|
742
|
+
if sign:
|
|
743
|
+
if required_insert_count <= delta_base:
|
|
744
|
+
raise QpackDecompressionFailed('invalid QPACK base')
|
|
745
|
+
base = required_insert_count - delta_base - 1
|
|
746
|
+
else:
|
|
747
|
+
base = required_insert_count + delta_base
|
|
748
|
+
headers: list[tuple[bytes, bytes]] = []
|
|
749
|
+
used_dynamic = False
|
|
750
|
+
while offset < len(data):
|
|
751
|
+
first = data[offset]
|
|
752
|
+
if first & 0x80:
|
|
753
|
+
static = bool(first & 0x40)
|
|
754
|
+
index, offset = decode_qpack_integer(data, offset, 6)
|
|
755
|
+
if static:
|
|
756
|
+
try:
|
|
757
|
+
headers.append(self.dynamic_table.lookup_static(index))
|
|
758
|
+
except ProtocolError as exc:
|
|
759
|
+
raise QpackDecompressionFailed('invalid QPACK static table index') from exc
|
|
760
|
+
else:
|
|
761
|
+
try:
|
|
762
|
+
absolute_index = self.dynamic_table.absolute_index_from_relative(base, index)
|
|
763
|
+
except ProtocolError as exc:
|
|
764
|
+
raise QpackDecompressionFailed('invalid QPACK relative reference') from exc
|
|
765
|
+
headers.append(self._require_dynamic_entry(absolute_index, required_insert_count=required_insert_count))
|
|
766
|
+
used_dynamic = True
|
|
767
|
+
continue
|
|
768
|
+
if first & 0x40:
|
|
769
|
+
static = bool(first & 0x10)
|
|
770
|
+
name_index, offset = decode_qpack_integer(data, offset, 4)
|
|
771
|
+
name = self._resolve_name(
|
|
772
|
+
static=static,
|
|
773
|
+
base=base,
|
|
774
|
+
index=name_index,
|
|
775
|
+
post_base=False,
|
|
776
|
+
required_insert_count=required_insert_count,
|
|
777
|
+
)
|
|
778
|
+
value, offset = decode_qpack_string(data, offset, 8)
|
|
779
|
+
headers.append((name, value))
|
|
780
|
+
if not static:
|
|
781
|
+
used_dynamic = True
|
|
782
|
+
continue
|
|
783
|
+
if first & 0x20:
|
|
784
|
+
name, offset = decode_qpack_string(data, offset, 4)
|
|
785
|
+
value, offset = decode_qpack_string(data, offset, 8)
|
|
786
|
+
headers.append((name, value))
|
|
787
|
+
continue
|
|
788
|
+
if first & 0x10:
|
|
789
|
+
index, offset = decode_qpack_integer(data, offset, 4)
|
|
790
|
+
try:
|
|
791
|
+
absolute_index = self.dynamic_table.absolute_index_from_post_base(base, index)
|
|
792
|
+
except ProtocolError as exc:
|
|
793
|
+
raise QpackDecompressionFailed('invalid QPACK post-base reference') from exc
|
|
794
|
+
headers.append(self._require_dynamic_entry(absolute_index, required_insert_count=required_insert_count))
|
|
795
|
+
used_dynamic = True
|
|
796
|
+
continue
|
|
797
|
+
name_index, offset = decode_qpack_integer(data, offset, 3)
|
|
798
|
+
name = self._resolve_name(
|
|
799
|
+
static=False,
|
|
800
|
+
base=base,
|
|
801
|
+
index=name_index,
|
|
802
|
+
post_base=True,
|
|
803
|
+
required_insert_count=required_insert_count,
|
|
804
|
+
)
|
|
805
|
+
value, offset = decode_qpack_string(data, offset, 8)
|
|
806
|
+
headers.append((name, value))
|
|
807
|
+
used_dynamic = True
|
|
808
|
+
self._unmark_blocked(stream_id, required_insert_count)
|
|
809
|
+
if required_insert_count != 0 and stream_id is not None:
|
|
810
|
+
self._pending_decoder_bytes.extend(encode_section_ack(stream_id))
|
|
811
|
+
return QpackFieldSection(
|
|
812
|
+
required_insert_count=required_insert_count,
|
|
813
|
+
base=base,
|
|
814
|
+
headers=headers,
|
|
815
|
+
used_dynamic=used_dynamic,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def cancel_stream(self, stream_id: int) -> None:
|
|
819
|
+
blocked = self._blocked_requirements.pop(stream_id, None)
|
|
820
|
+
if not blocked:
|
|
821
|
+
return
|
|
822
|
+
if self.dynamic_table.maximum_capacity <= 0:
|
|
823
|
+
return
|
|
824
|
+
self._pending_decoder_bytes.extend(encode_stream_cancellation(stream_id))
|
|
825
|
+
|
|
826
|
+
def take_decoder_stream_data(self) -> bytes:
|
|
827
|
+
payload = bytes(self._pending_decoder_bytes)
|
|
828
|
+
self._pending_decoder_bytes.clear()
|
|
829
|
+
return payload
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
# Stateless helpers preserve the previous convenience API but now emit/parse the
|
|
833
|
+
# RFC 9204 field-section prefix as well.
|
|
834
|
+
def encode_field_line(name: bytes, value: bytes) -> bytes:
|
|
835
|
+
return QpackEncoder(max_table_capacity=0).encode_field_section([(name, value)])
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def encode_field_section(headers: Iterable[tuple[bytes, bytes]]) -> bytes:
|
|
839
|
+
return QpackEncoder(max_table_capacity=0).encode_field_section(headers)
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def decode_field_section(data: bytes) -> list[tuple[bytes, bytes]]:
|
|
843
|
+
return QpackDecoder(max_table_capacity=0).decode_field_section(data, stream_id=None).headers
|