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/__init__.py +58 -0
- boring_http/client.py +692 -0
- boring_http/codings.py +39 -0
- boring_http/cookies.py +127 -0
- boring_http/errors.py +49 -0
- boring_http/hpack.py +247 -0
- boring_http/hpack_tables.py +326 -0
- boring_http/http1.py +259 -0
- boring_http/http2.py +685 -0
- boring_http/http3.py +551 -0
- boring_http/message.py +598 -0
- boring_http/pool.py +96 -0
- boring_http/py.typed +0 -0
- boring_http/qpack.py +245 -0
- boring_http/qpack_tables.py +104 -0
- boring_http/quic/__init__.py +0 -0
- boring_http/quic/connection.py +1261 -0
- boring_http/quic/packet.py +655 -0
- boring_http/quic/protocol.py +474 -0
- boring_http/quic/recovery.py +303 -0
- boring_http/quic/streams.py +154 -0
- boring_http/timing.py +63 -0
- boring_http/websocket.py +216 -0
- boring_http-1.0.0.dist-info/METADATA +303 -0
- boring_http-1.0.0.dist-info/RECORD +26 -0
- boring_http-1.0.0.dist-info/WHEEL +4 -0
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
|