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/codings.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import zlib
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
|
+
from compression import zstd
|
|
4
|
+
from typing import final
|
|
5
|
+
|
|
6
|
+
from boring_ssl2 import BrotliDecoder
|
|
7
|
+
|
|
8
|
+
from boring_http.errors import ProtocolError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@final
|
|
12
|
+
class ContentDecoder:
|
|
13
|
+
def __init__(self, codings: Sequence[str]) -> None:
|
|
14
|
+
self._codings = tuple(reversed(codings))
|
|
15
|
+
self._decoders = [self._decoder(coding) for coding in self._codings]
|
|
16
|
+
|
|
17
|
+
def decode(self, data: bytes) -> bytes:
|
|
18
|
+
for coding, decoder in zip(self._codings, self._decoders, strict=True):
|
|
19
|
+
try:
|
|
20
|
+
data = decoder(data)
|
|
21
|
+
except (zlib.error, ValueError, zstd.ZstdError) as error:
|
|
22
|
+
raise ProtocolError(f"the {coding} body is corrupt: {error}") from error
|
|
23
|
+
return data
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def _decoder(coding: str) -> Callable[[bytes], bytes]:
|
|
27
|
+
match coding:
|
|
28
|
+
case "gzip" | "x-gzip":
|
|
29
|
+
return zlib.decompressobj(16 + zlib.MAX_WBITS).decompress
|
|
30
|
+
case "deflate":
|
|
31
|
+
return zlib.decompressobj().decompress
|
|
32
|
+
case "br":
|
|
33
|
+
return BrotliDecoder().decode
|
|
34
|
+
case "zstd":
|
|
35
|
+
return zstd.ZstdDecompressor().decompress
|
|
36
|
+
case "identity":
|
|
37
|
+
return bytes
|
|
38
|
+
case _:
|
|
39
|
+
raise ProtocolError(f"unsupported content coding {coding!r}")
|
boring_http/cookies.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from collections.abc import Iterable, Iterator
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from email.utils import parsedate_to_datetime
|
|
5
|
+
from typing import Self, final
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@final
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class Cookie:
|
|
11
|
+
name: str
|
|
12
|
+
value: str
|
|
13
|
+
domain: str
|
|
14
|
+
path: str
|
|
15
|
+
expires: float | None
|
|
16
|
+
secure: bool
|
|
17
|
+
http_only: bool
|
|
18
|
+
host_only: bool
|
|
19
|
+
created: float
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def parse(cls, header: str, host: str, request_path: str) -> Self | None:
|
|
23
|
+
pair, _, rest = header.partition(";")
|
|
24
|
+
name, equals, value = pair.strip().partition("=")
|
|
25
|
+
if not equals or not name.strip():
|
|
26
|
+
return None
|
|
27
|
+
domain, path, expires, secure, http_only = "", "", None, False, False
|
|
28
|
+
for attribute in rest.split(";"):
|
|
29
|
+
key, _, argument = attribute.strip().partition("=")
|
|
30
|
+
match key.lower():
|
|
31
|
+
case "domain":
|
|
32
|
+
domain = argument.strip().lstrip(".").lower()
|
|
33
|
+
case "path":
|
|
34
|
+
path = argument.strip()
|
|
35
|
+
case "max-age" if (
|
|
36
|
+
argument.strip().lstrip("-").isascii()
|
|
37
|
+
and argument.strip().lstrip("-").isdigit()
|
|
38
|
+
):
|
|
39
|
+
expires = time.time() + int(argument.strip())
|
|
40
|
+
case "expires" if expires is None:
|
|
41
|
+
expires = cls._date(argument.strip())
|
|
42
|
+
case "secure":
|
|
43
|
+
secure = True
|
|
44
|
+
case "httponly":
|
|
45
|
+
http_only = True
|
|
46
|
+
case _:
|
|
47
|
+
pass
|
|
48
|
+
if domain and not (host == domain or host.endswith("." + domain)):
|
|
49
|
+
return None
|
|
50
|
+
if not path.startswith("/"):
|
|
51
|
+
path = request_path[: request_path.rfind("/")] or "/"
|
|
52
|
+
return cls(
|
|
53
|
+
name.strip(),
|
|
54
|
+
value.strip().strip('"'),
|
|
55
|
+
domain or host,
|
|
56
|
+
path,
|
|
57
|
+
expires,
|
|
58
|
+
secure,
|
|
59
|
+
http_only,
|
|
60
|
+
not domain,
|
|
61
|
+
time.time(),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def expired(self) -> bool:
|
|
66
|
+
return self.expires is not None and self.expires <= time.time()
|
|
67
|
+
|
|
68
|
+
def matches(self, host: str, path: str, secure: bool) -> bool:
|
|
69
|
+
if self.host_only:
|
|
70
|
+
domain_ok = host == self.domain
|
|
71
|
+
else:
|
|
72
|
+
domain_ok = host == self.domain or host.endswith("." + self.domain)
|
|
73
|
+
path_ok = path == self.path or (
|
|
74
|
+
path.startswith(self.path)
|
|
75
|
+
and (self.path.endswith("/") or path[len(self.path)] == "/")
|
|
76
|
+
)
|
|
77
|
+
return (
|
|
78
|
+
domain_ok and path_ok and (secure or not self.secure) and not self.expired
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def _date(text: str) -> float | None:
|
|
83
|
+
try:
|
|
84
|
+
return parsedate_to_datetime(text).timestamp()
|
|
85
|
+
except ValueError, TypeError:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@final
|
|
90
|
+
class CookieJar:
|
|
91
|
+
def __init__(self) -> None:
|
|
92
|
+
self._cookies: dict[tuple[str, str, str], Cookie] = {}
|
|
93
|
+
|
|
94
|
+
def __iter__(self) -> Iterator[Cookie]:
|
|
95
|
+
return iter(tuple(self._cookies.values()))
|
|
96
|
+
|
|
97
|
+
def __len__(self) -> int:
|
|
98
|
+
return len(self._cookies)
|
|
99
|
+
|
|
100
|
+
def get(self, name: str, domain: str | None = None) -> str | None:
|
|
101
|
+
for cookie in self._cookies.values():
|
|
102
|
+
if cookie.name == name and (domain is None or cookie.domain == domain):
|
|
103
|
+
return cookie.value
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
def set(self, cookie: Cookie) -> None:
|
|
107
|
+
key = (cookie.domain, cookie.path, cookie.name)
|
|
108
|
+
if cookie.expired:
|
|
109
|
+
_ = self._cookies.pop(key, None)
|
|
110
|
+
else:
|
|
111
|
+
self._cookies[key] = cookie
|
|
112
|
+
|
|
113
|
+
def store(self, cookies: Iterable[Cookie]) -> None:
|
|
114
|
+
for cookie in cookies:
|
|
115
|
+
self.set(cookie)
|
|
116
|
+
|
|
117
|
+
def clear(self) -> None:
|
|
118
|
+
self._cookies.clear()
|
|
119
|
+
|
|
120
|
+
def header(self, host: str, path: str, secure: bool) -> str | None:
|
|
121
|
+
matching = [
|
|
122
|
+
cookie
|
|
123
|
+
for cookie in self._cookies.values()
|
|
124
|
+
if cookie.matches(host, path, secure)
|
|
125
|
+
]
|
|
126
|
+
matching.sort(key=lambda cookie: (-len(cookie.path), cookie.created))
|
|
127
|
+
return "; ".join(f"{cookie.name}={cookie.value}" for cookie in matching) or None
|
boring_http/errors.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import builtins
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HTTPError(Exception):
|
|
5
|
+
pass
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConnectError(HTTPError):
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RemoteDisconnected(ConnectError):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StreamRefused(RemoteDisconnected):
|
|
17
|
+
# the server never processed the request, so sending it again is safe
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class StreamResetError(RemoteDisconnected):
|
|
22
|
+
def __init__(self, stream_id: int, code: int) -> None:
|
|
23
|
+
super().__init__(f"stream {stream_id} reset by the server with error {code:#x}")
|
|
24
|
+
self.stream_id: int = stream_id
|
|
25
|
+
self.code: int = code
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TimeoutError(HTTPError, builtins.TimeoutError):
|
|
29
|
+
def __init__(self, phase: str, seconds: float) -> None:
|
|
30
|
+
super().__init__(f"{phase} timed out after {seconds:g} seconds")
|
|
31
|
+
self.phase: str = phase
|
|
32
|
+
self.seconds: float = seconds
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ProtocolError(HTTPError):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TooManyRedirects(HTTPError):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class WebSocketClosed(HTTPError):
|
|
44
|
+
def __init__(self, code: int, reason: str) -> None:
|
|
45
|
+
super().__init__(
|
|
46
|
+
f"websocket closed with code {code}{': ' + reason if reason else ''}"
|
|
47
|
+
)
|
|
48
|
+
self.code: int = code
|
|
49
|
+
self.reason: str = reason
|
boring_http/hpack.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Final, final
|
|
3
|
+
|
|
4
|
+
from boring_http.errors import ProtocolError
|
|
5
|
+
from boring_http.hpack_tables import HUFFMAN_CODES, STATIC_TABLE
|
|
6
|
+
|
|
7
|
+
NEVER_INDEXED: Final = frozenset({b"authorization", b"proxy-authorization"})
|
|
8
|
+
ENTRY_OVERHEAD: Final = 32
|
|
9
|
+
DEFAULT_CAPACITY: Final = 4096
|
|
10
|
+
EOS: Final = 256
|
|
11
|
+
DECODE_TABLE: Final = {
|
|
12
|
+
(length, code): symbol for symbol, (code, length) in enumerate(HUFFMAN_CODES)
|
|
13
|
+
}
|
|
14
|
+
STATIC_PAIRS: Final = {pair: index for index, pair in enumerate(STATIC_TABLE, 1)}
|
|
15
|
+
STATIC_NAMES: Final = {
|
|
16
|
+
name: index for index, (name, _) in reversed(list(enumerate(STATIC_TABLE, 1)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@final
|
|
21
|
+
class BlockWriter:
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._data = bytearray()
|
|
24
|
+
|
|
25
|
+
def integer(self, value: int, prefix_bits: int, first_byte: int) -> None:
|
|
26
|
+
limit = (1 << prefix_bits) - 1
|
|
27
|
+
if value < limit:
|
|
28
|
+
self._data.append(first_byte | value)
|
|
29
|
+
return
|
|
30
|
+
self._data.append(first_byte | limit)
|
|
31
|
+
value -= limit
|
|
32
|
+
while value >= 128:
|
|
33
|
+
self._data.append(value % 128 + 128)
|
|
34
|
+
value //= 128
|
|
35
|
+
self._data.append(value)
|
|
36
|
+
|
|
37
|
+
def string(self, value: bytes, prefix_bits: int = 7, pattern: int = 0x00) -> None:
|
|
38
|
+
encoded = self.huffman(value)
|
|
39
|
+
if len(encoded) < len(value):
|
|
40
|
+
self.integer(len(encoded), prefix_bits, pattern | (1 << prefix_bits))
|
|
41
|
+
self._data += encoded
|
|
42
|
+
else:
|
|
43
|
+
self.integer(len(value), prefix_bits, pattern)
|
|
44
|
+
self._data += value
|
|
45
|
+
|
|
46
|
+
def finish(self) -> bytes:
|
|
47
|
+
return bytes(self._data)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def huffman(data: bytes) -> bytes:
|
|
51
|
+
out = bytearray()
|
|
52
|
+
bits, count = 0, 0
|
|
53
|
+
for byte in data:
|
|
54
|
+
code, length = HUFFMAN_CODES[byte]
|
|
55
|
+
bits = (bits << length) | code
|
|
56
|
+
count += length
|
|
57
|
+
while count >= 8:
|
|
58
|
+
count -= 8
|
|
59
|
+
out.append((bits >> count) & 0xFF)
|
|
60
|
+
bits &= (1 << count) - 1
|
|
61
|
+
if count:
|
|
62
|
+
out.append(((bits << (8 - count)) | ((1 << (8 - count)) - 1)) & 0xFF)
|
|
63
|
+
return bytes(out)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@final
|
|
67
|
+
class BlockReader:
|
|
68
|
+
def __init__(self, data: bytes) -> None:
|
|
69
|
+
self._data = data
|
|
70
|
+
self._position = 0
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def remaining(self) -> int:
|
|
74
|
+
return len(self._data) - self._position
|
|
75
|
+
|
|
76
|
+
def peek(self) -> int:
|
|
77
|
+
if self._position >= len(self._data):
|
|
78
|
+
raise ProtocolError("HPACK block ended inside a field")
|
|
79
|
+
return self._data[self._position]
|
|
80
|
+
|
|
81
|
+
def integer(self, prefix_bits: int) -> int:
|
|
82
|
+
limit = (1 << prefix_bits) - 1
|
|
83
|
+
value = self._byte() & limit
|
|
84
|
+
if value < limit:
|
|
85
|
+
return value
|
|
86
|
+
shift = 0
|
|
87
|
+
while True:
|
|
88
|
+
byte = self._byte()
|
|
89
|
+
value += (byte & 0x7F) << shift
|
|
90
|
+
shift += 7
|
|
91
|
+
if not byte & 0x80:
|
|
92
|
+
return value
|
|
93
|
+
if shift > 62:
|
|
94
|
+
raise ProtocolError("HPACK integer too large")
|
|
95
|
+
|
|
96
|
+
def string(self, prefix_bits: int = 7) -> bytes:
|
|
97
|
+
compressed = bool(self.peek() & (1 << prefix_bits))
|
|
98
|
+
length = self.integer(prefix_bits)
|
|
99
|
+
if length > self.remaining:
|
|
100
|
+
raise ProtocolError("HPACK string runs past the block")
|
|
101
|
+
value = self._data[self._position : self._position + length]
|
|
102
|
+
self._position += length
|
|
103
|
+
return self.huffman(value) if compressed else value
|
|
104
|
+
|
|
105
|
+
def _byte(self) -> int:
|
|
106
|
+
if self._position >= len(self._data):
|
|
107
|
+
raise ProtocolError("HPACK block ended inside a field")
|
|
108
|
+
value = self._data[self._position]
|
|
109
|
+
self._position += 1
|
|
110
|
+
return value
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def huffman(data: bytes) -> bytes:
|
|
114
|
+
out = bytearray()
|
|
115
|
+
code, length = 0, 0
|
|
116
|
+
for byte in data:
|
|
117
|
+
for bit in range(7, -1, -1):
|
|
118
|
+
code = (code << 1) | ((byte >> bit) & 1)
|
|
119
|
+
length += 1
|
|
120
|
+
symbol = DECODE_TABLE.get((length, code))
|
|
121
|
+
if symbol is not None:
|
|
122
|
+
if symbol == EOS:
|
|
123
|
+
raise ProtocolError("EOS inside HPACK huffman data")
|
|
124
|
+
out.append(symbol)
|
|
125
|
+
code, length = 0, 0
|
|
126
|
+
if length > 7 or code != (1 << length) - 1:
|
|
127
|
+
raise ProtocolError("invalid HPACK huffman padding")
|
|
128
|
+
return bytes(out)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@final
|
|
132
|
+
class Encoder:
|
|
133
|
+
def __init__(self) -> None:
|
|
134
|
+
self._table: list[tuple[bytes, bytes]] = []
|
|
135
|
+
self._size = 0
|
|
136
|
+
self._capacity = DEFAULT_CAPACITY
|
|
137
|
+
self._announce: int | None = None
|
|
138
|
+
|
|
139
|
+
def set_capacity(self, capacity: int) -> None:
|
|
140
|
+
capacity = min(capacity, DEFAULT_CAPACITY)
|
|
141
|
+
if capacity != self._capacity:
|
|
142
|
+
self._capacity = capacity
|
|
143
|
+
self._announce = capacity
|
|
144
|
+
self._evict()
|
|
145
|
+
|
|
146
|
+
def encode(self, headers: Iterable[tuple[bytes, bytes]]) -> bytes:
|
|
147
|
+
writer = BlockWriter()
|
|
148
|
+
if self._announce is not None:
|
|
149
|
+
writer.integer(self._announce, 5, 0x20)
|
|
150
|
+
self._announce = None
|
|
151
|
+
for name, value in headers:
|
|
152
|
+
pair_index, name_index = self._find(name, value)
|
|
153
|
+
if pair_index is not None:
|
|
154
|
+
writer.integer(pair_index, 7, 0x80)
|
|
155
|
+
continue
|
|
156
|
+
if name in NEVER_INDEXED:
|
|
157
|
+
writer.integer(name_index or 0, 4, 0x10)
|
|
158
|
+
else:
|
|
159
|
+
writer.integer(name_index or 0, 6, 0x40)
|
|
160
|
+
if name_index is None:
|
|
161
|
+
writer.string(name)
|
|
162
|
+
writer.string(value)
|
|
163
|
+
if name not in NEVER_INDEXED:
|
|
164
|
+
self._add(name, value)
|
|
165
|
+
return writer.finish()
|
|
166
|
+
|
|
167
|
+
def _find(self, name: bytes, value: bytes) -> tuple[int | None, int | None]:
|
|
168
|
+
pair_index = STATIC_PAIRS.get((name, value))
|
|
169
|
+
if pair_index is not None:
|
|
170
|
+
return pair_index, pair_index
|
|
171
|
+
name_index = STATIC_NAMES.get(name)
|
|
172
|
+
for position, entry in enumerate(self._table):
|
|
173
|
+
if entry == (name, value):
|
|
174
|
+
return len(STATIC_TABLE) + 1 + position, None
|
|
175
|
+
if name_index is None and entry[0] == name:
|
|
176
|
+
name_index = len(STATIC_TABLE) + 1 + position
|
|
177
|
+
return None, name_index
|
|
178
|
+
|
|
179
|
+
def _add(self, name: bytes, value: bytes) -> None:
|
|
180
|
+
self._table.insert(0, (name, value))
|
|
181
|
+
self._size += len(name) + len(value) + ENTRY_OVERHEAD
|
|
182
|
+
self._evict()
|
|
183
|
+
|
|
184
|
+
def _evict(self) -> None:
|
|
185
|
+
while self._size > self._capacity:
|
|
186
|
+
name, value = self._table.pop()
|
|
187
|
+
self._size -= len(name) + len(value) + ENTRY_OVERHEAD
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@final
|
|
191
|
+
class Decoder:
|
|
192
|
+
def __init__(self, capacity: int, max_header_list_size: int) -> None:
|
|
193
|
+
self._table: list[tuple[bytes, bytes]] = []
|
|
194
|
+
self._size = 0
|
|
195
|
+
self._capacity = capacity
|
|
196
|
+
self._limit = capacity
|
|
197
|
+
self._max_list = max_header_list_size
|
|
198
|
+
|
|
199
|
+
def decode(self, block: bytes) -> list[tuple[bytes, bytes]]:
|
|
200
|
+
reader = BlockReader(block)
|
|
201
|
+
headers: list[tuple[bytes, bytes]] = []
|
|
202
|
+
total = 0
|
|
203
|
+
while reader.remaining:
|
|
204
|
+
first = reader.peek()
|
|
205
|
+
if first & 0x80:
|
|
206
|
+
name, value = self._lookup(reader.integer(7))
|
|
207
|
+
elif first & 0x40:
|
|
208
|
+
index = reader.integer(6)
|
|
209
|
+
name = self._lookup(index)[0] if index else reader.string()
|
|
210
|
+
value = reader.string()
|
|
211
|
+
self._add(name, value)
|
|
212
|
+
elif first & 0x20:
|
|
213
|
+
size = reader.integer(5)
|
|
214
|
+
if size > self._limit:
|
|
215
|
+
raise ProtocolError(
|
|
216
|
+
f"HPACK table size update {size} exceeds the advertised {self._limit}"
|
|
217
|
+
)
|
|
218
|
+
self._capacity = size
|
|
219
|
+
self._evict()
|
|
220
|
+
continue
|
|
221
|
+
else:
|
|
222
|
+
index = reader.integer(4)
|
|
223
|
+
name = self._lookup(index)[0] if index else reader.string()
|
|
224
|
+
value = reader.string()
|
|
225
|
+
headers.append((name, value))
|
|
226
|
+
total += len(name) + len(value) + ENTRY_OVERHEAD
|
|
227
|
+
if total > self._max_list:
|
|
228
|
+
raise ProtocolError(f"header list exceeds {self._max_list} bytes")
|
|
229
|
+
return headers
|
|
230
|
+
|
|
231
|
+
def _lookup(self, index: int) -> tuple[bytes, bytes]:
|
|
232
|
+
if 1 <= index <= len(STATIC_TABLE):
|
|
233
|
+
return STATIC_TABLE[index - 1]
|
|
234
|
+
position = index - len(STATIC_TABLE) - 1
|
|
235
|
+
if 0 <= position < len(self._table):
|
|
236
|
+
return self._table[position]
|
|
237
|
+
raise ProtocolError(f"HPACK index {index} is not in the table")
|
|
238
|
+
|
|
239
|
+
def _add(self, name: bytes, value: bytes) -> None:
|
|
240
|
+
self._table.insert(0, (name, value))
|
|
241
|
+
self._size += len(name) + len(value) + ENTRY_OVERHEAD
|
|
242
|
+
self._evict()
|
|
243
|
+
|
|
244
|
+
def _evict(self) -> None:
|
|
245
|
+
while self._size > self._capacity:
|
|
246
|
+
name, value = self._table.pop()
|
|
247
|
+
self._size -= len(name) + len(value) + ENTRY_OVERHEAD
|