boring-ssl2 2.0.1__py3-none-win_amd64.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_ssl2/__init__.py +27 -0
- boring_ssl2/binding.py +101 -0
- boring_ssl2/boring_ssl2.dll +0 -0
- boring_ssl2/buffer.py +247 -0
- boring_ssl2/compression.py +148 -0
- boring_ssl2/contexts.py +419 -0
- boring_ssl2/crypto_functions.py +121 -0
- boring_ssl2/errors.py +14 -0
- boring_ssl2/library.py +32 -0
- boring_ssl2/licenses/BORINGSSL +238 -0
- boring_ssl2/licenses/BROTLI +19 -0
- boring_ssl2/profile.py +286 -0
- boring_ssl2/py.typed +0 -0
- boring_ssl2/quic.py +359 -0
- boring_ssl2/revocation.py +136 -0
- boring_ssl2/ssl_functions.py +403 -0
- boring_ssl2/x509_functions.py +105 -0
- boring_ssl2-2.0.1.dist-info/METADATA +308 -0
- boring_ssl2-2.0.1.dist-info/RECORD +21 -0
- boring_ssl2-2.0.1.dist-info/WHEEL +5 -0
- boring_ssl2-2.0.1.dist-info/licenses/LICENSE +201 -0
boring_ssl2/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from boring_ssl2.buffer import TLSBuffer
|
|
2
|
+
from boring_ssl2.compression import BrotliDecoder
|
|
3
|
+
from boring_ssl2.contexts import BoringSSL, ClientContext, ServerContext
|
|
4
|
+
from boring_ssl2.errors import BoringSSLError, CertificateVerificationError
|
|
5
|
+
from boring_ssl2.library import Library
|
|
6
|
+
from boring_ssl2.profile import ClientHelloProfile
|
|
7
|
+
from boring_ssl2.quic import EncryptionLevel, PacketProtection, QuicHandshake, QuicVersion, Suite
|
|
8
|
+
from boring_ssl2.revocation import CertificateRevocationList, ParsedCertificate
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"BoringSSL",
|
|
12
|
+
"BoringSSLError",
|
|
13
|
+
"BrotliDecoder",
|
|
14
|
+
"CertificateRevocationList",
|
|
15
|
+
"CertificateVerificationError",
|
|
16
|
+
"ClientContext",
|
|
17
|
+
"ClientHelloProfile",
|
|
18
|
+
"EncryptionLevel",
|
|
19
|
+
"Library",
|
|
20
|
+
"PacketProtection",
|
|
21
|
+
"ParsedCertificate",
|
|
22
|
+
"QuicHandshake",
|
|
23
|
+
"QuicVersion",
|
|
24
|
+
"ServerContext",
|
|
25
|
+
"Suite",
|
|
26
|
+
"TLSBuffer",
|
|
27
|
+
)
|
boring_ssl2/binding.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from ctypes import (
|
|
5
|
+
CFUNCTYPE,
|
|
6
|
+
c_char_p,
|
|
7
|
+
c_int,
|
|
8
|
+
c_long,
|
|
9
|
+
c_size_t,
|
|
10
|
+
c_ssize_t,
|
|
11
|
+
c_uint,
|
|
12
|
+
c_uint8,
|
|
13
|
+
c_uint16,
|
|
14
|
+
c_uint32,
|
|
15
|
+
c_void_p,
|
|
16
|
+
)
|
|
17
|
+
from enum import IntEnum, StrEnum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from ssl2.errors import ConfigurationError
|
|
22
|
+
|
|
23
|
+
from boring_ssl2.errors import BoringSSLError
|
|
24
|
+
|
|
25
|
+
type Foreign = type[c_char_p | c_int | c_long | c_size_t | c_ssize_t | c_uint | c_uint16 | c_uint32 | c_void_p]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FileName(StrEnum):
|
|
29
|
+
# what CMake installs next to this package on each platform
|
|
30
|
+
LINUX = "libboring_ssl2.so"
|
|
31
|
+
DARWIN = "libboring_ssl2.dylib"
|
|
32
|
+
WINDOWS = "boring_ssl2.dll"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Size(IntEnum):
|
|
36
|
+
ERROR_STRING = 256
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Callback:
|
|
40
|
+
# the C function pointer types BoringSSL and brotli call back into
|
|
41
|
+
CERTIFICATE_COMPRESS = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p, c_size_t)
|
|
42
|
+
CERTIFICATE_DECOMPRESS = CFUNCTYPE(c_int, c_void_p, c_void_p, c_size_t, c_void_p, c_size_t)
|
|
43
|
+
PROTOCOL_SELECT = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p, c_void_p, c_uint, c_void_p)
|
|
44
|
+
SERVER_NAME = CFUNCTYPE(c_int, c_void_p, c_void_p, c_void_p)
|
|
45
|
+
NEW_SESSION = CFUNCTYPE(c_int, c_void_p, c_void_p)
|
|
46
|
+
KEY_LOG = CFUNCTYPE(None, c_void_p, c_char_p)
|
|
47
|
+
QUIC_SET_SECRET = CFUNCTYPE(c_int, c_void_p, c_int, c_void_p, c_void_p, c_size_t)
|
|
48
|
+
QUIC_ADD_HANDSHAKE_DATA = CFUNCTYPE(c_int, c_void_p, c_int, c_void_p, c_size_t)
|
|
49
|
+
QUIC_FLUSH_FLIGHT = CFUNCTYPE(c_int, c_void_p)
|
|
50
|
+
QUIC_SEND_ALERT = CFUNCTYPE(c_int, c_void_p, c_int, c_uint8)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LibraryPath:
|
|
54
|
+
@staticmethod
|
|
55
|
+
def bundled() -> Path:
|
|
56
|
+
directory = Path(__file__).parent
|
|
57
|
+
if sys.platform == "win32":
|
|
58
|
+
name = FileName.WINDOWS
|
|
59
|
+
elif sys.platform in ("darwin", "ios"):
|
|
60
|
+
name = FileName.DARWIN
|
|
61
|
+
else:
|
|
62
|
+
name = FileName.LINUX
|
|
63
|
+
path = directory / name
|
|
64
|
+
if not path.exists():
|
|
65
|
+
raise ConfigurationError(
|
|
66
|
+
f"no BoringSSL build at {path}; build one with cmake (see README.md) or pass Library(path=...)"
|
|
67
|
+
)
|
|
68
|
+
return path
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class Binder:
|
|
72
|
+
# loads one shared object and binds its functions the first time each is used, so a build that lacks
|
|
73
|
+
# a function fails at the call that needs it and names it
|
|
74
|
+
def __init__(self, path: Path | str | None = None) -> None:
|
|
75
|
+
self.path = Path(path) if path is not None else LibraryPath.bundled()
|
|
76
|
+
self._handle = ctypes.CDLL(str(self.path))
|
|
77
|
+
|
|
78
|
+
def bind(self, name: str, result: Foreign | None, *arguments: Foreign) -> Callable[..., Any]:
|
|
79
|
+
try:
|
|
80
|
+
function = self._handle[name]
|
|
81
|
+
except AttributeError:
|
|
82
|
+
raise ConfigurationError(
|
|
83
|
+
f"{self.path} has no {name}; this BoringSSL build predates the feature that needs it"
|
|
84
|
+
) from None
|
|
85
|
+
function.restype = result
|
|
86
|
+
function.argtypes = arguments
|
|
87
|
+
return function
|
|
88
|
+
|
|
89
|
+
def errors(self) -> tuple[str, ...]:
|
|
90
|
+
# drains BoringSSL's error queue into readable reasons
|
|
91
|
+
get_error = self.bind("ERR_get_error", c_uint32)
|
|
92
|
+
error_string = self.bind("ERR_error_string_n", c_char_p, c_uint32, c_void_p, c_size_t)
|
|
93
|
+
text = ctypes.create_string_buffer(Size.ERROR_STRING)
|
|
94
|
+
messages: list[str] = []
|
|
95
|
+
while code := get_error():
|
|
96
|
+
error_string(code, text, len(text))
|
|
97
|
+
messages.append(ctypes.string_at(text).decode("ascii", "replace"))
|
|
98
|
+
return tuple(messages)
|
|
99
|
+
|
|
100
|
+
def error(self, operation: str) -> BoringSSLError:
|
|
101
|
+
return BoringSSLError(operation, self.errors())
|
|
Binary file
|
boring_ssl2/buffer.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from collections.abc import Buffer, Callable
|
|
3
|
+
from enum import IntEnum
|
|
4
|
+
from typing import overload
|
|
5
|
+
|
|
6
|
+
from ssl2.errors import EarlyDataRejectedError, RaggedEOF, WantReadError, WantWriteError
|
|
7
|
+
from ssl2.interface import ClientContext, Limit, ServerContext
|
|
8
|
+
from ssl2.registry import CipherSuite, NextProtocol, ProtocolVersion, TLSVersion
|
|
9
|
+
|
|
10
|
+
from boring_ssl2.errors import BoringSSLError, CertificateVerificationError
|
|
11
|
+
from boring_ssl2.library import Library
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SSLErrorCode(IntEnum):
|
|
15
|
+
# SSL_get_error
|
|
16
|
+
NONE = 0
|
|
17
|
+
SSL = 1
|
|
18
|
+
WANT_READ = 2
|
|
19
|
+
WANT_WRITE = 3
|
|
20
|
+
SYSCALL = 5
|
|
21
|
+
ZERO_RETURN = 6
|
|
22
|
+
EARLY_DATA_REJECTED = 15
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VerifyResult(IntEnum):
|
|
26
|
+
OK = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ExDataIndex(IntEnum):
|
|
30
|
+
# slot 0 is SSL_set_app_data; the new session callback reads the cache key back from it
|
|
31
|
+
SESSION_KEY = 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CInt(IntEnum):
|
|
35
|
+
# the most SSL_read and SSL_write take in one call
|
|
36
|
+
MAXIMUM = 2**31 - 1
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TLSBuffer:
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
context: ClientContext | ServerContext,
|
|
43
|
+
library: Library,
|
|
44
|
+
context_handle: int,
|
|
45
|
+
*,
|
|
46
|
+
server: bool,
|
|
47
|
+
session_key: str | None = None,
|
|
48
|
+
) -> None:
|
|
49
|
+
self._context = context
|
|
50
|
+
self._library = library
|
|
51
|
+
handle = library.SSL_new(context_handle)
|
|
52
|
+
if handle is None:
|
|
53
|
+
raise library.error("SSL_new")
|
|
54
|
+
self._handle = handle
|
|
55
|
+
incoming, outgoing = library.BIO_new(library.BIO_s_mem()), library.BIO_new(library.BIO_s_mem())
|
|
56
|
+
if incoming is None or outgoing is None:
|
|
57
|
+
raise library.error("BIO_new")
|
|
58
|
+
self._incoming = incoming
|
|
59
|
+
self._outgoing = outgoing
|
|
60
|
+
library.SSL_set_bio(handle, incoming, outgoing)
|
|
61
|
+
if server:
|
|
62
|
+
library.SSL_set_accept_state(handle)
|
|
63
|
+
else:
|
|
64
|
+
library.SSL_set_connect_state(handle)
|
|
65
|
+
self._closing = False
|
|
66
|
+
self._storage = ctypes.create_string_buffer(Limit.RECEIVE)
|
|
67
|
+
self._session_key = None if session_key is None else ctypes.create_string_buffer(session_key.encode())
|
|
68
|
+
if self._session_key is not None:
|
|
69
|
+
library.check(
|
|
70
|
+
library.SSL_set_ex_data(handle, ExDataIndex.SESSION_KEY, ctypes.addressof(self._session_key)),
|
|
71
|
+
"SSL_set_ex_data",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __del__(self) -> None:
|
|
75
|
+
self._library.SSL_free(self._handle)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def handle(self) -> int:
|
|
79
|
+
return self._handle
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def library(self) -> Library:
|
|
83
|
+
return self._library
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def context(self) -> ClientContext | ServerContext:
|
|
87
|
+
return self._context
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def session_reused(self) -> bool:
|
|
91
|
+
return self._library.SSL_session_reused(self._handle) == 1
|
|
92
|
+
|
|
93
|
+
@overload
|
|
94
|
+
def read(self, amount: int) -> bytes: ...
|
|
95
|
+
|
|
96
|
+
@overload
|
|
97
|
+
def read(self, amount: int, buffer: Buffer) -> int: ...
|
|
98
|
+
|
|
99
|
+
def read(self, amount: int, buffer: Buffer | None = None) -> bytes | int:
|
|
100
|
+
if buffer is None:
|
|
101
|
+
size = min(amount, CInt.MAXIMUM)
|
|
102
|
+
storage = (
|
|
103
|
+
(ctypes.c_char * size).from_buffer(self._storage)
|
|
104
|
+
if size <= Limit.RECEIVE
|
|
105
|
+
else ctypes.create_string_buffer(size)
|
|
106
|
+
)
|
|
107
|
+
count = self._read_into(storage)
|
|
108
|
+
return ctypes.string_at(storage, count)
|
|
109
|
+
view = memoryview(buffer).cast("B")
|
|
110
|
+
return self._read_into((ctypes.c_char * min(amount, view.nbytes, CInt.MAXIMUM)).from_buffer(view))
|
|
111
|
+
|
|
112
|
+
def write(self, data: Buffer) -> int:
|
|
113
|
+
payload = bytes(data)
|
|
114
|
+
if not payload:
|
|
115
|
+
# BoringSSL holds post-handshake tickets until the first write; a zero-length write releases them
|
|
116
|
+
self._library.SSL_write(self._handle, payload, 0)
|
|
117
|
+
return 0
|
|
118
|
+
size = min(len(payload), CInt.MAXIMUM)
|
|
119
|
+
return self._operate(lambda: self._library.SSL_write(self._handle, payload, size), "write")
|
|
120
|
+
|
|
121
|
+
def attach(self, fileno: int) -> None:
|
|
122
|
+
# the socket becomes BoringSSL's own BIO: records move between the kernel and the TLS state with no stop here
|
|
123
|
+
self._library.check(self._library.SSL_set_fd(self._handle, fileno), "SSL_set_fd")
|
|
124
|
+
|
|
125
|
+
def do_handshake(self) -> None:
|
|
126
|
+
self._operate(lambda: self._library.SSL_do_handshake(self._handle), "handshake")
|
|
127
|
+
|
|
128
|
+
def cipher(self) -> CipherSuite | int | None:
|
|
129
|
+
cipher = self._library.SSL_get_current_cipher(self._handle)
|
|
130
|
+
return None if cipher is None else CipherSuite(self._library.SSL_CIPHER_get_protocol_id(cipher))
|
|
131
|
+
|
|
132
|
+
def negotiated_protocol(self) -> NextProtocol | bytes | None:
|
|
133
|
+
data, length = ctypes.c_void_p(), ctypes.c_uint()
|
|
134
|
+
self._library.SSL_get0_alpn_selected(self._handle, ctypes.byref(data), ctypes.byref(length))
|
|
135
|
+
if data.value is None or not length.value:
|
|
136
|
+
return None
|
|
137
|
+
selected = ctypes.string_at(data.value, length.value)
|
|
138
|
+
try:
|
|
139
|
+
return NextProtocol(selected)
|
|
140
|
+
except ValueError:
|
|
141
|
+
return selected
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def negotiated_tls_version(self) -> TLSVersion | None:
|
|
145
|
+
if not self._library.SSL_is_init_finished(self._handle):
|
|
146
|
+
return None
|
|
147
|
+
return ProtocolVersion(self._library.SSL_version(self._handle)).tls_version
|
|
148
|
+
|
|
149
|
+
def shutdown(self) -> None:
|
|
150
|
+
if not self._closing:
|
|
151
|
+
self._library.ERR_clear_error()
|
|
152
|
+
result = self._library.SSL_shutdown(self._handle)
|
|
153
|
+
if result < 0:
|
|
154
|
+
failure = self._failure(result, "shutdown")
|
|
155
|
+
if failure is not None:
|
|
156
|
+
raise failure
|
|
157
|
+
self._closing = True
|
|
158
|
+
if result == 1:
|
|
159
|
+
return
|
|
160
|
+
# BoringSSL refuses application data that arrives before the peer's close_notify, so it is read away
|
|
161
|
+
storage = ctypes.create_string_buffer(Limit.RECEIVE)
|
|
162
|
+
while self._read_into(storage):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
def process_incoming(self, data_from_network: bytes) -> None:
|
|
166
|
+
written = self._library.BIO_write(self._incoming, data_from_network, len(data_from_network))
|
|
167
|
+
if written != len(data_from_network):
|
|
168
|
+
raise self._library.error("BIO_write")
|
|
169
|
+
|
|
170
|
+
def incoming_bytes_buffered(self) -> int:
|
|
171
|
+
return self._library.BIO_ctrl_pending(self._incoming)
|
|
172
|
+
|
|
173
|
+
def process_outgoing(self, amount_bytes_for_network: int) -> bytes:
|
|
174
|
+
storage = ctypes.create_string_buffer(min(amount_bytes_for_network, CInt.MAXIMUM))
|
|
175
|
+
count = self._library.BIO_read(self._outgoing, storage, len(storage))
|
|
176
|
+
return ctypes.string_at(storage, count) if count > 0 else b""
|
|
177
|
+
|
|
178
|
+
def outgoing_bytes_buffered(self) -> int:
|
|
179
|
+
return self._library.BIO_ctrl_pending(self._outgoing)
|
|
180
|
+
|
|
181
|
+
def getpeercert(self) -> bytes | None:
|
|
182
|
+
chain = self.getpeerchain()
|
|
183
|
+
return chain[0] if chain else None
|
|
184
|
+
|
|
185
|
+
def getpeerchain(self) -> tuple[bytes, ...]:
|
|
186
|
+
library = self._library
|
|
187
|
+
stack = library.SSL_get0_peer_certificates(self._handle)
|
|
188
|
+
if stack is None:
|
|
189
|
+
return ()
|
|
190
|
+
certificates: list[bytes] = []
|
|
191
|
+
for index in range(library.OPENSSL_sk_num(stack)):
|
|
192
|
+
item = library.OPENSSL_sk_value(stack, index)
|
|
193
|
+
certificates.append(ctypes.string_at(library.CRYPTO_BUFFER_data(item), library.CRYPTO_BUFFER_len(item)))
|
|
194
|
+
return tuple(certificates)
|
|
195
|
+
|
|
196
|
+
def _read_into(self, storage: ctypes.Array[ctypes.c_char]) -> int:
|
|
197
|
+
size = len(storage)
|
|
198
|
+
if not size:
|
|
199
|
+
return 0
|
|
200
|
+
handle, library = self._handle, self._library
|
|
201
|
+
total = self._operate(lambda: library.SSL_read(handle, storage, size), "read")
|
|
202
|
+
# one SSL_read returns one record; the records already in the memory BIO or the kernel follow in this call.
|
|
203
|
+
# An attached socket is non-blocking, so BoringSSL answers WANT_READ when they run out, never a block
|
|
204
|
+
while 0 < total < size:
|
|
205
|
+
rest = (ctypes.c_char * (size - total)).from_buffer(storage, total)
|
|
206
|
+
more = library.SSL_read(handle, rest, size - total)
|
|
207
|
+
if more <= 0:
|
|
208
|
+
break
|
|
209
|
+
total += more
|
|
210
|
+
return total
|
|
211
|
+
|
|
212
|
+
def _operate(self, call: Callable[[], int], operation: str) -> int:
|
|
213
|
+
self._library.ERR_clear_error()
|
|
214
|
+
result = call()
|
|
215
|
+
if result > 0:
|
|
216
|
+
return result
|
|
217
|
+
failure = self._failure(result, operation)
|
|
218
|
+
if failure is None:
|
|
219
|
+
return 0
|
|
220
|
+
raise failure
|
|
221
|
+
|
|
222
|
+
def _failure(self, result: int, operation: str) -> Exception | None:
|
|
223
|
+
library = self._library
|
|
224
|
+
code = library.SSL_get_error(self._handle, result)
|
|
225
|
+
reasons = library.errors()
|
|
226
|
+
match code:
|
|
227
|
+
case SSLErrorCode.WANT_READ:
|
|
228
|
+
return WantReadError(f"{operation} needs more data from the peer")
|
|
229
|
+
case SSLErrorCode.WANT_WRITE:
|
|
230
|
+
return WantWriteError(f"{operation} needs the outgoing buffer drained")
|
|
231
|
+
case SSLErrorCode.ZERO_RETURN:
|
|
232
|
+
return None
|
|
233
|
+
case SSLErrorCode.EARLY_DATA_REJECTED:
|
|
234
|
+
return EarlyDataRejectedError(f"{operation}: the server declined the early data")
|
|
235
|
+
case SSLErrorCode.SSL:
|
|
236
|
+
verdict = library.SSL_get_verify_result(self._handle)
|
|
237
|
+
if verdict != VerifyResult.OK:
|
|
238
|
+
return CertificateVerificationError(
|
|
239
|
+
operation, reasons, library.X509_verify_cert_error_string(verdict).decode("ascii")
|
|
240
|
+
)
|
|
241
|
+
return BoringSSLError(operation, reasons)
|
|
242
|
+
case SSLErrorCode.SYSCALL:
|
|
243
|
+
# end of stream, a reset or a timeout on the socket: the connection is gone either way
|
|
244
|
+
detail = f": {'; '.join(reasons)}" if reasons else ""
|
|
245
|
+
return RaggedEOF(f"{operation}: the connection ended without close_notify{detail}")
|
|
246
|
+
case _:
|
|
247
|
+
return BoringSSLError(operation, (f"SSL_get_error={code}", *reasons))
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import zlib
|
|
3
|
+
from compression import zstd
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
|
|
6
|
+
from ssl2.errors import ConfigurationError
|
|
7
|
+
from ssl2.registry import CertificateCompressionAlgorithm
|
|
8
|
+
|
|
9
|
+
from boring_ssl2.binding import Callback
|
|
10
|
+
from boring_ssl2.library import Library
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BrotliEncoderSetting(IntEnum):
|
|
14
|
+
# the quality, window and mode brotli's one-shot encoder takes
|
|
15
|
+
QUALITY = 11
|
|
16
|
+
WINDOW = 22
|
|
17
|
+
MODE_GENERIC = 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BrotliResult(IntEnum):
|
|
21
|
+
# what the one-shot calls and the stream decoder answer
|
|
22
|
+
FALSE = 0
|
|
23
|
+
TRUE = 1
|
|
24
|
+
NEEDS_MORE_INPUT = 2
|
|
25
|
+
NEEDS_MORE_OUTPUT = 3
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Size(IntEnum):
|
|
29
|
+
OUTPUT_CHUNK = 65536
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BrotliDecoder:
|
|
33
|
+
# a streaming decoder for brotli bodies, fed in pieces as they arrive
|
|
34
|
+
def __init__(self, library: Library | None = None) -> None:
|
|
35
|
+
self._library = library or Library.default()
|
|
36
|
+
state = self._library.BrotliDecoderCreateInstance(None, None, None)
|
|
37
|
+
if state is None:
|
|
38
|
+
raise MemoryError("brotli decoder state")
|
|
39
|
+
self._state = state
|
|
40
|
+
self.finished = False
|
|
41
|
+
|
|
42
|
+
def __del__(self) -> None:
|
|
43
|
+
self._library.BrotliDecoderDestroyInstance(self._state)
|
|
44
|
+
|
|
45
|
+
def decode(self, data: bytes) -> bytes:
|
|
46
|
+
storage = ctypes.create_string_buffer(data, len(data))
|
|
47
|
+
available_in = ctypes.c_size_t(len(data))
|
|
48
|
+
next_in = ctypes.c_void_p(ctypes.addressof(storage))
|
|
49
|
+
chunks: list[bytes] = []
|
|
50
|
+
while True:
|
|
51
|
+
output = ctypes.create_string_buffer(Size.OUTPUT_CHUNK)
|
|
52
|
+
available_out = ctypes.c_size_t(Size.OUTPUT_CHUNK)
|
|
53
|
+
next_out = ctypes.c_void_p(ctypes.addressof(output))
|
|
54
|
+
result = self._library.BrotliDecoderDecompressStream(
|
|
55
|
+
self._state,
|
|
56
|
+
ctypes.byref(available_in),
|
|
57
|
+
ctypes.byref(next_in),
|
|
58
|
+
ctypes.byref(available_out),
|
|
59
|
+
ctypes.byref(next_out),
|
|
60
|
+
None,
|
|
61
|
+
)
|
|
62
|
+
chunks.append(output.raw[: Size.OUTPUT_CHUNK - available_out.value])
|
|
63
|
+
match result:
|
|
64
|
+
case BrotliResult.NEEDS_MORE_OUTPUT:
|
|
65
|
+
continue
|
|
66
|
+
case BrotliResult.NEEDS_MORE_INPUT:
|
|
67
|
+
return b"".join(chunks)
|
|
68
|
+
case BrotliResult.TRUE:
|
|
69
|
+
self.finished = True
|
|
70
|
+
return b"".join(chunks)
|
|
71
|
+
case _:
|
|
72
|
+
raise ValueError("corrupt brotli stream")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CertificateCompression:
|
|
76
|
+
# RFC 8879: the client decompresses the server's certificate, the server compresses its own
|
|
77
|
+
def __init__(self, library: Library) -> None:
|
|
78
|
+
self._library = library
|
|
79
|
+
self._callbacks: list[object] = []
|
|
80
|
+
|
|
81
|
+
def register(self, handle: int, algorithm: CertificateCompressionAlgorithm, *, client: bool) -> None:
|
|
82
|
+
if not algorithm.is_assigned:
|
|
83
|
+
raise ConfigurationError(f"no certificate compression codec for {algorithm.name}")
|
|
84
|
+
library = self._library
|
|
85
|
+
|
|
86
|
+
def compress(_ssl: int, out: int, data: int, length: int) -> int:
|
|
87
|
+
try:
|
|
88
|
+
encoded = self._encode(algorithm, ctypes.string_at(data, length))
|
|
89
|
+
except ValueError, zlib.error, zstd.ZstdError:
|
|
90
|
+
return BrotliResult.FALSE
|
|
91
|
+
return library.CBB_add_bytes(out, encoded, len(encoded))
|
|
92
|
+
|
|
93
|
+
def decompress(_ssl: int, out: int, size: int, data: int, length: int) -> int:
|
|
94
|
+
try:
|
|
95
|
+
decoded = self._decode(algorithm, ctypes.string_at(data, length), size)
|
|
96
|
+
except ValueError, zlib.error, zstd.ZstdError:
|
|
97
|
+
return BrotliResult.FALSE
|
|
98
|
+
buffer = library.CRYPTO_BUFFER_new(decoded, len(decoded), None)
|
|
99
|
+
if len(decoded) != size or buffer is None:
|
|
100
|
+
return BrotliResult.FALSE
|
|
101
|
+
ctypes.c_void_p.from_address(out).value = buffer
|
|
102
|
+
return BrotliResult.TRUE
|
|
103
|
+
|
|
104
|
+
compressor = None if client else Callback.CERTIFICATE_COMPRESS(compress)
|
|
105
|
+
decompressor = Callback.CERTIFICATE_DECOMPRESS(decompress) if client else None
|
|
106
|
+
self._callbacks += (compressor, decompressor)
|
|
107
|
+
library.check(
|
|
108
|
+
library.SSL_CTX_add_cert_compression_alg(handle, algorithm, compressor, decompressor),
|
|
109
|
+
"SSL_CTX_add_cert_compression_alg",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def _encode(self, algorithm: CertificateCompressionAlgorithm, data: bytes) -> bytes:
|
|
113
|
+
match algorithm:
|
|
114
|
+
case CertificateCompressionAlgorithm.zlib:
|
|
115
|
+
return zlib.compress(data)
|
|
116
|
+
case CertificateCompressionAlgorithm.zstd:
|
|
117
|
+
return zstd.compress(data)
|
|
118
|
+
case CertificateCompressionAlgorithm.brotli:
|
|
119
|
+
library = self._library
|
|
120
|
+
capacity = library.BrotliEncoderMaxCompressedSize(len(data))
|
|
121
|
+
storage = ctypes.create_string_buffer(capacity)
|
|
122
|
+
size = ctypes.c_size_t(capacity)
|
|
123
|
+
accepted = library.BrotliEncoderCompress(
|
|
124
|
+
BrotliEncoderSetting.QUALITY,
|
|
125
|
+
BrotliEncoderSetting.WINDOW,
|
|
126
|
+
BrotliEncoderSetting.MODE_GENERIC,
|
|
127
|
+
len(data),
|
|
128
|
+
data,
|
|
129
|
+
ctypes.byref(size),
|
|
130
|
+
storage,
|
|
131
|
+
)
|
|
132
|
+
if accepted != BrotliResult.TRUE:
|
|
133
|
+
raise ValueError("brotli compression failed")
|
|
134
|
+
return storage.raw[: size.value]
|
|
135
|
+
|
|
136
|
+
def _decode(self, algorithm: CertificateCompressionAlgorithm, data: bytes, size: int) -> bytes:
|
|
137
|
+
match algorithm:
|
|
138
|
+
case CertificateCompressionAlgorithm.zlib:
|
|
139
|
+
return zlib.decompress(data, bufsize=size)
|
|
140
|
+
case CertificateCompressionAlgorithm.zstd:
|
|
141
|
+
return zstd.decompress(data)
|
|
142
|
+
case CertificateCompressionAlgorithm.brotli:
|
|
143
|
+
storage = ctypes.create_string_buffer(size)
|
|
144
|
+
decoded = ctypes.c_size_t(size)
|
|
145
|
+
accepted = self._library.BrotliDecoderDecompress(len(data), data, ctypes.byref(decoded), storage)
|
|
146
|
+
if accepted != BrotliResult.TRUE:
|
|
147
|
+
raise ValueError("brotli decompression failed")
|
|
148
|
+
return storage.raw[: decoded.value]
|