boring-http 1.0.0__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.
boring_http/qpack.py ADDED
@@ -0,0 +1,245 @@
1
+ import asyncio
2
+ from collections.abc import Iterable
3
+ from typing import Final, final
4
+
5
+ from boring_http.errors import ProtocolError
6
+ from boring_http.hpack import BlockReader, BlockWriter
7
+ from boring_http.qpack_tables import STATIC_TABLE
8
+
9
+ STATIC_PAIRS: Final = {pair: index for index, pair in enumerate(STATIC_TABLE)}
10
+ STATIC_NAMES: Final = {
11
+ name: index for index, (name, _) in reversed(list(enumerate(STATIC_TABLE)))
12
+ }
13
+ ENTRY_OVERHEAD: Final = 32
14
+ ENCODER_STREAM_LIMIT: Final = 1 << 20
15
+
16
+
17
+ @final
18
+ class Qpack:
19
+ def __init__(self, maximum_capacity: int) -> None:
20
+ self._maximum = maximum_capacity
21
+ self._capacity = 0
22
+ self._entries: list[tuple[bytes, bytes]] = []
23
+ self._dropped = 0
24
+ self._size = 0
25
+ self._insert_count = 0
26
+ self._pending = bytearray()
27
+ self._instructions = BlockWriter()
28
+ self._waiters: list[asyncio.Future[None]] = []
29
+
30
+ def encode(self, headers: Iterable[tuple[bytes, bytes]]) -> bytes:
31
+ writer = BlockWriter()
32
+ writer.integer(0, 8, 0x00)
33
+ writer.integer(0, 7, 0x00)
34
+ for name, value in headers:
35
+ index = STATIC_PAIRS.get((name, value))
36
+ if index is not None:
37
+ writer.integer(index, 6, 0xC0)
38
+ continue
39
+ name_index = STATIC_NAMES.get(name)
40
+ if name_index is not None:
41
+ writer.integer(name_index, 4, 0x50)
42
+ else:
43
+ writer.string(name, 3, 0x20)
44
+ writer.string(value)
45
+ return writer.finish()
46
+
47
+ def decode(self, block: bytes, stream_id: int) -> list[tuple[bytes, bytes]] | None:
48
+ reader = BlockReader(block)
49
+ required = self._required_insert_count(reader.integer(8))
50
+ if required > self._insert_count:
51
+ return None
52
+ sign = bool(reader.peek() & 0x80)
53
+ delta = reader.integer(7)
54
+ base = required - delta - 1 if sign else required + delta
55
+ headers: list[tuple[bytes, bytes]] = []
56
+ while reader.remaining:
57
+ first = reader.peek()
58
+ if first & 0x80:
59
+ index = reader.integer(6)
60
+ headers.append(
61
+ self._static(index)
62
+ if first & 0x40
63
+ else self._dynamic(base - 1 - index)
64
+ )
65
+ elif first & 0x40:
66
+ index = reader.integer(4)
67
+ name = (
68
+ self._static(index)
69
+ if first & 0x10
70
+ else self._dynamic(base - 1 - index)
71
+ )[0]
72
+ headers.append((name, reader.string()))
73
+ elif first & 0x20:
74
+ name = reader.string(3)
75
+ headers.append((name, reader.string()))
76
+ elif first & 0x10:
77
+ headers.append(self._dynamic(base + reader.integer(4)))
78
+ else:
79
+ name = self._dynamic(base + reader.integer(3))[0]
80
+ headers.append((name, reader.string()))
81
+ if required:
82
+ self._instructions.integer(stream_id, 7, 0x80)
83
+ return headers
84
+
85
+ def feed_encoder(self, data: bytes) -> None:
86
+ self._pending += data
87
+ if len(self._pending) > ENCODER_STREAM_LIMIT:
88
+ raise ProtocolError("QPACK encoder instruction exceeds 1 MiB")
89
+ before = self._insert_count
90
+ while self._pending:
91
+ consumed = self._instruction(bytes(self._pending))
92
+ if consumed == 0:
93
+ break
94
+ del self._pending[:consumed]
95
+ if self._insert_count > before:
96
+ self._instructions.integer(self._insert_count - before, 6, 0x00)
97
+ for waiter in self._waiters:
98
+ if not waiter.done():
99
+ waiter.set_result(None)
100
+ self._waiters.clear()
101
+
102
+ def cancel(self, stream_id: int) -> None:
103
+ self._instructions.integer(stream_id, 6, 0x40)
104
+
105
+ def take_instructions(self) -> bytes:
106
+ data = self._instructions.finish()
107
+ self._instructions = BlockWriter()
108
+ return data
109
+
110
+ async def wait(self) -> None:
111
+ waiter = asyncio.get_running_loop().create_future()
112
+ self._waiters.append(waiter)
113
+ await waiter
114
+
115
+ def _instruction(self, data: bytes) -> int:
116
+ first = data[0]
117
+ if first & 0x80:
118
+ parsed = self._integer(data, 0, 6)
119
+ if parsed is None:
120
+ return 0
121
+ index, position = parsed
122
+ value = self._string(data, position)
123
+ if value is None:
124
+ return 0
125
+ name = (
126
+ self._static(index)
127
+ if first & 0x40
128
+ else self._dynamic(self._insert_count - 1 - index)
129
+ )[0]
130
+ self._insert(name, value[0])
131
+ return value[1]
132
+ if first & 0x40:
133
+ name = self._string(data, 0, 5)
134
+ if name is None:
135
+ return 0
136
+ value = self._string(data, name[1])
137
+ if value is None:
138
+ return 0
139
+ self._insert(name[0], value[0])
140
+ return value[1]
141
+ parsed = self._integer(data, 0, 5)
142
+ if parsed is None:
143
+ return 0
144
+ if first & 0x20:
145
+ self._set_capacity(parsed[0])
146
+ else:
147
+ name, value = self._dynamic(self._insert_count - 1 - parsed[0])
148
+ self._insert(name, value)
149
+ return parsed[1]
150
+
151
+ def _insert(self, name: bytes, value: bytes) -> None:
152
+ size = len(name) + len(value) + ENTRY_OVERHEAD
153
+ if size > self._capacity:
154
+ raise ProtocolError("QPACK entry larger than the dynamic table")
155
+ while self._size + size > self._capacity:
156
+ self._evict()
157
+ self._entries.append((name, value))
158
+ self._size += size
159
+ self._insert_count += 1
160
+
161
+ def _set_capacity(self, capacity: int) -> None:
162
+ if capacity > self._maximum:
163
+ raise ProtocolError(
164
+ f"QPACK capacity {capacity} above the advertised {self._maximum}"
165
+ )
166
+ self._capacity = capacity
167
+ while self._size > capacity:
168
+ self._evict()
169
+
170
+ def _evict(self) -> None:
171
+ name, value = self._entries.pop(0)
172
+ self._size -= len(name) + len(value) + ENTRY_OVERHEAD
173
+ self._dropped += 1
174
+
175
+ def _dynamic(self, absolute: int) -> tuple[bytes, bytes]:
176
+ position = absolute - self._dropped
177
+ if not 0 <= position < len(self._entries):
178
+ raise ProtocolError(
179
+ f"QPACK dynamic table index {absolute} is not available"
180
+ )
181
+ return self._entries[position]
182
+
183
+ def _required_insert_count(self, encoded: int) -> int:
184
+ # RFC 9204 section 4.5.1.1
185
+ if encoded == 0:
186
+ return 0
187
+ full_range = 2 * (self._maximum // ENTRY_OVERHEAD)
188
+ if encoded > full_range:
189
+ raise ProtocolError("QPACK required insert count outside its range")
190
+ maximum = self._insert_count + self._maximum // ENTRY_OVERHEAD
191
+ wrap = maximum // full_range * full_range
192
+ required = wrap + encoded - 1
193
+ if required > maximum:
194
+ if required <= full_range:
195
+ raise ProtocolError("QPACK required insert count outside its range")
196
+ required -= full_range
197
+ if required == 0:
198
+ raise ProtocolError("QPACK required insert count outside its range")
199
+ return required
200
+
201
+ @staticmethod
202
+ def _static(index: int) -> tuple[bytes, bytes]:
203
+ if index >= len(STATIC_TABLE):
204
+ raise ProtocolError(f"QPACK static index {index} out of range")
205
+ return STATIC_TABLE[index]
206
+
207
+ @staticmethod
208
+ def _integer(
209
+ data: bytes, position: int, prefix_bits: int
210
+ ) -> tuple[int, int] | None:
211
+ if position >= len(data):
212
+ return None
213
+ limit = (1 << prefix_bits) - 1
214
+ value = data[position] & limit
215
+ position += 1
216
+ if value < limit:
217
+ return value, position
218
+ shift = 0
219
+ while True:
220
+ if position >= len(data):
221
+ return None
222
+ byte = data[position]
223
+ position += 1
224
+ value += (byte & 0x7F) << shift
225
+ shift += 7
226
+ if not byte & 0x80:
227
+ return value, position
228
+ if shift > 62:
229
+ raise ProtocolError("QPACK integer too large")
230
+
231
+ @classmethod
232
+ def _string(
233
+ cls, data: bytes, position: int, prefix_bits: int = 7
234
+ ) -> tuple[bytes, int] | None:
235
+ if position >= len(data):
236
+ return None
237
+ compressed = bool(data[position] & (1 << prefix_bits))
238
+ parsed = cls._integer(data, position, prefix_bits)
239
+ if parsed is None:
240
+ return None
241
+ length, position = parsed
242
+ if position + length > len(data):
243
+ return None
244
+ value = data[position : position + length]
245
+ return (BlockReader.huffman(value) if compressed else value), position + length
@@ -0,0 +1,104 @@
1
+ # generated from the text of RFC 9204 appendix A, so regenerate rather than edit
2
+ from typing import Final
3
+
4
+ STATIC_TABLE: Final[tuple[tuple[bytes, bytes], ...]] = (
5
+ (b":authority", b""),
6
+ (b":path", b"/"),
7
+ (b"age", b"0"),
8
+ (b"content-disposition", b""),
9
+ (b"content-length", b"0"),
10
+ (b"cookie", b""),
11
+ (b"date", b""),
12
+ (b"etag", b""),
13
+ (b"if-modified-since", b""),
14
+ (b"if-none-match", b""),
15
+ (b"last-modified", b""),
16
+ (b"link", b""),
17
+ (b"location", b""),
18
+ (b"referer", b""),
19
+ (b"set-cookie", b""),
20
+ (b":method", b"CONNECT"),
21
+ (b":method", b"DELETE"),
22
+ (b":method", b"GET"),
23
+ (b":method", b"HEAD"),
24
+ (b":method", b"OPTIONS"),
25
+ (b":method", b"POST"),
26
+ (b":method", b"PUT"),
27
+ (b":scheme", b"http"),
28
+ (b":scheme", b"https"),
29
+ (b":status", b"103"),
30
+ (b":status", b"200"),
31
+ (b":status", b"304"),
32
+ (b":status", b"404"),
33
+ (b":status", b"503"),
34
+ (b"accept", b"*/*"),
35
+ (b"accept", b"application/dns-"),
36
+ (b"accept-encoding", b"gzip, deflate, br"),
37
+ (b"accept-ranges", b"bytes"),
38
+ (b"access-control-allow-headers", b"cache-control"),
39
+ (b"access-control-allow-headers", b"content-type"),
40
+ (b"access-control-allow-origin", b"*"),
41
+ (b"cache-control", b"max-age=0"),
42
+ (b"cache-control", b"max-age=2592000"),
43
+ (b"cache-control", b"max-age=604800"),
44
+ (b"cache-control", b"no-cache"),
45
+ (b"cache-control", b"no-store"),
46
+ (b"cache-control", b"public, max-"),
47
+ (b"content-encoding", b"br"),
48
+ (b"content-encoding", b"gzip"),
49
+ (b"content-type", b"application/dns-"),
50
+ (b"content-type", b"application/"),
51
+ (b"content-type", b"application/json"),
52
+ (b"content-type", b"application/x-www-"),
53
+ (b"content-type", b"image/gif"),
54
+ (b"content-type", b"image/jpeg"),
55
+ (b"content-type", b"image/png"),
56
+ (b"content-type", b"text/css"),
57
+ (b"content-type", b"text/html;"),
58
+ (b"content-type", b"text/plain"),
59
+ (b"content-type", b"text/"),
60
+ (b"range", b"bytes=0-"),
61
+ (b"strict-transport-security", b"max-age=31536000"),
62
+ (b"strict-transport-security", b"max-age=31536000;"),
63
+ (b"strict-transport-security", b"max-age=31536000;"),
64
+ (b"vary", b"accept-encoding"),
65
+ (b"vary", b"origin"),
66
+ (b"x-content-type-options", b"nosniff"),
67
+ (b"x-xss-protection", b"1; mode=block"),
68
+ (b":status", b"100"),
69
+ (b":status", b"204"),
70
+ (b":status", b"206"),
71
+ (b":status", b"302"),
72
+ (b":status", b"400"),
73
+ (b":status", b"403"),
74
+ (b":status", b"421"),
75
+ (b":status", b"425"),
76
+ (b":status", b"500"),
77
+ (b"accept-language", b""),
78
+ (b"access-control-allow-credentials", b"FALSE"),
79
+ (b"access-control-allow-credentials", b"TRUE"),
80
+ (b"access-control-allow-headers", b"*"),
81
+ (b"access-control-allow-methods", b"get"),
82
+ (b"access-control-allow-methods", b"get, post, options"),
83
+ (b"access-control-allow-methods", b"options"),
84
+ (b"access-control-expose-headers", b"content-length"),
85
+ (b"access-control-request-headers", b"content-type"),
86
+ (b"access-control-request-method", b"get"),
87
+ (b"access-control-request-method", b"post"),
88
+ (b"alt-svc", b"clear"),
89
+ (b"authorization", b""),
90
+ (b"content-security-policy", b"script-src 'none';"),
91
+ (b"early-data", b"1"),
92
+ (b"expect-ct", b""),
93
+ (b"forwarded", b""),
94
+ (b"if-range", b""),
95
+ (b"origin", b""),
96
+ (b"purpose", b"prefetch"),
97
+ (b"server", b""),
98
+ (b"timing-allow-origin", b"*"),
99
+ (b"upgrade-insecure-requests", b"1"),
100
+ (b"user-agent", b""),
101
+ (b"x-forwarded-for", b""),
102
+ (b"x-frame-options", b"deny"),
103
+ (b"x-frame-options", b"sameorigin"),
104
+ )
File without changes