ssl2 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.
- ssl2/__init__.py +106 -0
- ssl2/asynchronous.py +217 -0
- ssl2/client_hello.py +353 -0
- ssl2/credentials.py +125 -0
- ssl2/errors.py +22 -0
- ssl2/fingerprint.py +326 -0
- ssl2/interface.py +398 -0
- ssl2/parameters.py +731 -0
- ssl2/proxy.py +308 -0
- ssl2/py.typed +0 -0
- ssl2-1.0.0.dist-info/METADATA +180 -0
- ssl2-1.0.0.dist-info/RECORD +13 -0
- ssl2-1.0.0.dist-info/WHEEL +4 -0
ssl2/interface.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import errno
|
|
2
|
+
import socket
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from collections.abc import Buffer, Callable, Sequence
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from threading import Lock
|
|
7
|
+
from typing import Final, Protocol, Self, final, overload
|
|
8
|
+
|
|
9
|
+
from ssl2.client_hello import ClientHello
|
|
10
|
+
from ssl2.credentials import SigningChain, TrustStore
|
|
11
|
+
from ssl2.errors import ConfigurationError, RaggedEOF, WantReadError, WantWriteError
|
|
12
|
+
from ssl2.parameters import CipherSuite, NextProtocol, TLSVersion
|
|
13
|
+
from ssl2.proxy import Proxy, ProxyError, ProxyScheme
|
|
14
|
+
|
|
15
|
+
RECEIVE_SIZE: Final = 65536
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class TLSClientConfiguration:
|
|
20
|
+
certificate_chain: SigningChain | None = None
|
|
21
|
+
ciphers: Sequence[CipherSuite] | None = None
|
|
22
|
+
inner_protocols: Sequence[NextProtocol | bytes] | None = None
|
|
23
|
+
lowest_supported_version: TLSVersion | None = None
|
|
24
|
+
highest_supported_version: TLSVersion | None = None
|
|
25
|
+
trust_store: TrustStore | None = None
|
|
26
|
+
client_hello: ClientHello | None = None
|
|
27
|
+
verify_certificates: bool = True
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
described = (
|
|
31
|
+
self.ciphers,
|
|
32
|
+
self.inner_protocols,
|
|
33
|
+
self.lowest_supported_version,
|
|
34
|
+
self.highest_supported_version,
|
|
35
|
+
)
|
|
36
|
+
if self.client_hello is not None and any(
|
|
37
|
+
value is not None for value in described
|
|
38
|
+
):
|
|
39
|
+
raise ConfigurationError(
|
|
40
|
+
"client_hello already describes the ciphers, inner protocols and versions"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True, slots=True)
|
|
45
|
+
class TLSServerConfiguration:
|
|
46
|
+
certificate_chain: Sequence[SigningChain] | None = None
|
|
47
|
+
ciphers: Sequence[CipherSuite] | None = None
|
|
48
|
+
inner_protocols: Sequence[NextProtocol | bytes] | None = None
|
|
49
|
+
lowest_supported_version: TLSVersion | None = None
|
|
50
|
+
highest_supported_version: TLSVersion | None = None
|
|
51
|
+
trust_store: TrustStore | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class TLSBuffer(Protocol):
|
|
55
|
+
@overload
|
|
56
|
+
def read(self, amt: int) -> bytes: ...
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def read(self, amt: int, buffer: Buffer) -> int: ...
|
|
60
|
+
|
|
61
|
+
def write(self, buf: Buffer) -> int: ...
|
|
62
|
+
|
|
63
|
+
def do_handshake(self) -> None: ...
|
|
64
|
+
|
|
65
|
+
def cipher(self) -> CipherSuite | int | None: ...
|
|
66
|
+
|
|
67
|
+
def negotiated_protocol(self) -> NextProtocol | bytes | None: ...
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def context(self) -> ClientContext | ServerContext: ...
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def negotiated_tls_version(self) -> TLSVersion | None: ...
|
|
74
|
+
|
|
75
|
+
def shutdown(self) -> None: ...
|
|
76
|
+
|
|
77
|
+
def process_incoming(self, data_from_network: bytes) -> None: ...
|
|
78
|
+
|
|
79
|
+
def incoming_bytes_buffered(self) -> int: ...
|
|
80
|
+
|
|
81
|
+
def process_outgoing(self, amount_bytes_for_network: int) -> bytes: ...
|
|
82
|
+
|
|
83
|
+
def outgoing_bytes_buffered(self) -> int: ...
|
|
84
|
+
|
|
85
|
+
def getpeercert(self) -> bytes | None: ...
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ClientContext(Protocol):
|
|
89
|
+
def __init__(self, configuration: TLSClientConfiguration) -> None: ...
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def configuration(self) -> TLSClientConfiguration: ...
|
|
93
|
+
|
|
94
|
+
def connect(self, address: tuple[str | None, int]) -> TLSSocket: ...
|
|
95
|
+
|
|
96
|
+
def create_buffer(self, server_hostname: str) -> TLSBuffer: ...
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ServerContext(Protocol):
|
|
100
|
+
def __init__(self, configuration: TLSServerConfiguration) -> None: ...
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def configuration(self) -> TLSServerConfiguration: ...
|
|
104
|
+
|
|
105
|
+
def connect(self, address: tuple[str | None, int]) -> TLSSocket: ...
|
|
106
|
+
|
|
107
|
+
def create_buffer(self) -> TLSBuffer: ...
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass(frozen=True, slots=True)
|
|
111
|
+
class TLSImplementation[ClientContextT: ClientContext, ServerContextT: ServerContext]:
|
|
112
|
+
client_context: type[ClientContextT]
|
|
113
|
+
server_context: type[ServerContextT]
|
|
114
|
+
validate_config: Callable[[TLSClientConfiguration | TLSServerConfiguration], None]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@final
|
|
118
|
+
class SessionCache:
|
|
119
|
+
def __init__(self, limit: int = 1024) -> None:
|
|
120
|
+
self._entries: OrderedDict[str, tuple[bytes, bool]] = OrderedDict()
|
|
121
|
+
self._limit = limit
|
|
122
|
+
self._lock = Lock()
|
|
123
|
+
|
|
124
|
+
def store(self, key: str, session: bytes, *, single_use: bool) -> None:
|
|
125
|
+
with self._lock:
|
|
126
|
+
self._entries[key] = (session, single_use)
|
|
127
|
+
self._entries.move_to_end(key)
|
|
128
|
+
while len(self._entries) > self._limit:
|
|
129
|
+
_ = self._entries.popitem(last=False)
|
|
130
|
+
|
|
131
|
+
def take(self, key: str) -> bytes | None:
|
|
132
|
+
with self._lock:
|
|
133
|
+
entry = self._entries.get(key)
|
|
134
|
+
if entry is None:
|
|
135
|
+
return None
|
|
136
|
+
session, single_use = entry
|
|
137
|
+
if single_use:
|
|
138
|
+
del self._entries[key]
|
|
139
|
+
return session
|
|
140
|
+
|
|
141
|
+
def __len__(self) -> int:
|
|
142
|
+
return len(self._entries)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Transport(Protocol):
|
|
146
|
+
def recv(self, bufsize: int, /) -> bytes: ...
|
|
147
|
+
|
|
148
|
+
def send(self, data: Buffer, /) -> int: ...
|
|
149
|
+
|
|
150
|
+
def close(self) -> None: ...
|
|
151
|
+
|
|
152
|
+
def getsockname(self) -> tuple[str | None, int]: ...
|
|
153
|
+
|
|
154
|
+
def getpeername(self) -> tuple[str | None, int]: ...
|
|
155
|
+
|
|
156
|
+
def settimeout(self, value: float | None, /) -> None: ...
|
|
157
|
+
|
|
158
|
+
def gettimeout(self) -> float | None: ...
|
|
159
|
+
|
|
160
|
+
def fileno(self) -> int: ...
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True, slots=True)
|
|
164
|
+
class Listening:
|
|
165
|
+
listener: socket.socket
|
|
166
|
+
accept: Callable[[], TLSBuffer]
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass(frozen=True, slots=True)
|
|
170
|
+
class Connected:
|
|
171
|
+
buffer: TLSBuffer
|
|
172
|
+
transport: Transport
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@final
|
|
176
|
+
class TLSSocket:
|
|
177
|
+
def __init__(
|
|
178
|
+
self, context: ClientContext | ServerContext, state: Listening | Connected
|
|
179
|
+
) -> None:
|
|
180
|
+
self._context = context
|
|
181
|
+
self._state = state
|
|
182
|
+
self._unsent = b""
|
|
183
|
+
|
|
184
|
+
@classmethod
|
|
185
|
+
def connect(
|
|
186
|
+
cls,
|
|
187
|
+
context: ClientContext,
|
|
188
|
+
address: tuple[str, int],
|
|
189
|
+
*,
|
|
190
|
+
proxy: Proxy | None = None,
|
|
191
|
+
timeout: float | None = None,
|
|
192
|
+
) -> Self:
|
|
193
|
+
transport: Transport
|
|
194
|
+
if proxy is None:
|
|
195
|
+
transport = socket.create_connection(address, timeout=timeout)
|
|
196
|
+
elif proxy.scheme is ProxyScheme.HTTPS:
|
|
197
|
+
transport = cls.connect(context, (proxy.host, proxy.port), timeout=timeout)
|
|
198
|
+
else:
|
|
199
|
+
transport = socket.create_connection(
|
|
200
|
+
(proxy.host, proxy.port), timeout=timeout
|
|
201
|
+
)
|
|
202
|
+
try:
|
|
203
|
+
leftover = b"" if proxy is None else cls._tunnel(proxy, address, transport)
|
|
204
|
+
return cls.open(
|
|
205
|
+
context, context.create_buffer(address[0]), transport, leftover
|
|
206
|
+
)
|
|
207
|
+
except BaseException:
|
|
208
|
+
transport.close()
|
|
209
|
+
raise
|
|
210
|
+
|
|
211
|
+
@classmethod
|
|
212
|
+
def open(
|
|
213
|
+
cls,
|
|
214
|
+
context: ClientContext | ServerContext,
|
|
215
|
+
buffer: TLSBuffer,
|
|
216
|
+
transport: Transport,
|
|
217
|
+
leftover: bytes = b"",
|
|
218
|
+
) -> Self:
|
|
219
|
+
self = cls(context, Connected(buffer, transport))
|
|
220
|
+
if leftover:
|
|
221
|
+
buffer.process_incoming(leftover)
|
|
222
|
+
self._run(buffer.do_handshake)
|
|
223
|
+
return self
|
|
224
|
+
|
|
225
|
+
@classmethod
|
|
226
|
+
def bind(cls, context: ServerContext, address: tuple[str | None, int]) -> Self:
|
|
227
|
+
host, port = address
|
|
228
|
+
family = socket.AF_INET6 if host and ":" in host else socket.AF_INET
|
|
229
|
+
return cls(
|
|
230
|
+
context,
|
|
231
|
+
Listening(
|
|
232
|
+
socket.create_server((host or "", port), family=family),
|
|
233
|
+
context.create_buffer,
|
|
234
|
+
),
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
@property
|
|
238
|
+
def context(self) -> ClientContext | ServerContext:
|
|
239
|
+
return self._context
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def buffer(self) -> TLSBuffer:
|
|
243
|
+
return self._connected().buffer
|
|
244
|
+
|
|
245
|
+
def recv(self, bufsize: int) -> bytes:
|
|
246
|
+
buffer = self._connected().buffer
|
|
247
|
+
try:
|
|
248
|
+
return self._run(lambda: buffer.read(bufsize))
|
|
249
|
+
except RaggedEOF:
|
|
250
|
+
# the ssl module's default: a peer that drops TCP without close_notify reads as end of stream
|
|
251
|
+
return b""
|
|
252
|
+
|
|
253
|
+
def send(self, bytes: Buffer) -> int:
|
|
254
|
+
buffer = self._connected().buffer
|
|
255
|
+
return self._run(lambda: buffer.write(bytes))
|
|
256
|
+
|
|
257
|
+
def close(self, force: bool = False) -> None:
|
|
258
|
+
if isinstance(self._state, Listening):
|
|
259
|
+
self._state.listener.close()
|
|
260
|
+
return
|
|
261
|
+
connection = self._state
|
|
262
|
+
try:
|
|
263
|
+
if force:
|
|
264
|
+
self._shutdown_once(connection)
|
|
265
|
+
else:
|
|
266
|
+
self._run(connection.buffer.shutdown)
|
|
267
|
+
except RaggedEOF, OSError:
|
|
268
|
+
pass
|
|
269
|
+
connection.transport.close()
|
|
270
|
+
|
|
271
|
+
def listen(self, backlog: int) -> None:
|
|
272
|
+
self._listening().listener.listen(backlog)
|
|
273
|
+
|
|
274
|
+
def accept(self) -> tuple[Self, tuple[str | None, int]]:
|
|
275
|
+
listening = self._listening()
|
|
276
|
+
accepted: tuple[socket.socket, tuple[str | None, int]] = (
|
|
277
|
+
listening.listener.accept()
|
|
278
|
+
)
|
|
279
|
+
return type(self).open(
|
|
280
|
+
self._context, listening.accept(), accepted[0]
|
|
281
|
+
), accepted[1]
|
|
282
|
+
|
|
283
|
+
def getsockname(self) -> tuple[str | None, int]:
|
|
284
|
+
return self._endpoint().getsockname()
|
|
285
|
+
|
|
286
|
+
def getpeername(self) -> tuple[str | None, int]:
|
|
287
|
+
return self._endpoint().getpeername()
|
|
288
|
+
|
|
289
|
+
def getpeercert(self) -> bytes | None:
|
|
290
|
+
return self._connected().buffer.getpeercert()
|
|
291
|
+
|
|
292
|
+
def cipher(self) -> CipherSuite | int | None:
|
|
293
|
+
return self._connected().buffer.cipher()
|
|
294
|
+
|
|
295
|
+
def negotiated_protocol(self) -> NextProtocol | bytes | None:
|
|
296
|
+
return self._connected().buffer.negotiated_protocol()
|
|
297
|
+
|
|
298
|
+
@property
|
|
299
|
+
def negotiated_tls_version(self) -> TLSVersion | None:
|
|
300
|
+
return self._connected().buffer.negotiated_tls_version
|
|
301
|
+
|
|
302
|
+
def settimeout(self, value: float | None) -> None:
|
|
303
|
+
self._endpoint().settimeout(value)
|
|
304
|
+
|
|
305
|
+
def gettimeout(self) -> float | None:
|
|
306
|
+
return self._endpoint().gettimeout()
|
|
307
|
+
|
|
308
|
+
def fileno(self) -> int:
|
|
309
|
+
return self._endpoint().fileno()
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def _tunnel(
|
|
313
|
+
cls, proxy: Proxy, destination: tuple[str, int], transport: Transport
|
|
314
|
+
) -> bytes:
|
|
315
|
+
host, port = destination
|
|
316
|
+
if (family := proxy.scheme.local_address_family) is not None:
|
|
317
|
+
host = str(
|
|
318
|
+
socket.getaddrinfo(host, port, family, socket.SOCK_STREAM)[0][4][0]
|
|
319
|
+
)
|
|
320
|
+
conversation = proxy.converse((host, port))
|
|
321
|
+
pending = bytearray()
|
|
322
|
+
step = next(conversation)
|
|
323
|
+
while True:
|
|
324
|
+
request, expected = step
|
|
325
|
+
view = memoryview(request)
|
|
326
|
+
while view:
|
|
327
|
+
view = view[transport.send(view) :]
|
|
328
|
+
while not (end := Proxy.reply_length(pending, expected)):
|
|
329
|
+
piece = transport.recv(RECEIVE_SIZE)
|
|
330
|
+
if not piece:
|
|
331
|
+
raise ProxyError(
|
|
332
|
+
"the proxy closed the connection during the tunnel handshake"
|
|
333
|
+
)
|
|
334
|
+
pending += piece
|
|
335
|
+
reply = bytes(pending[:end])
|
|
336
|
+
del pending[:end]
|
|
337
|
+
try:
|
|
338
|
+
step = conversation.send(reply)
|
|
339
|
+
except StopIteration:
|
|
340
|
+
return bytes(pending)
|
|
341
|
+
|
|
342
|
+
def _connected(self) -> Connected:
|
|
343
|
+
if isinstance(self._state, Listening):
|
|
344
|
+
raise OSError(errno.ENOTCONN, "the socket is listening, not connected")
|
|
345
|
+
return self._state
|
|
346
|
+
|
|
347
|
+
def _listening(self) -> Listening:
|
|
348
|
+
if isinstance(self._state, Connected):
|
|
349
|
+
raise OSError(errno.EINVAL, "the socket is connected, not listening")
|
|
350
|
+
return self._state
|
|
351
|
+
|
|
352
|
+
def _endpoint(self) -> Transport:
|
|
353
|
+
return (
|
|
354
|
+
self._state.listener
|
|
355
|
+
if isinstance(self._state, Listening)
|
|
356
|
+
else self._state.transport
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
def _run[ResultT](self, operation: Callable[[], ResultT]) -> ResultT:
|
|
360
|
+
connection = self._connected()
|
|
361
|
+
while True:
|
|
362
|
+
try:
|
|
363
|
+
result = operation()
|
|
364
|
+
except WantReadError:
|
|
365
|
+
self._flush(connection)
|
|
366
|
+
self._fill(connection)
|
|
367
|
+
continue
|
|
368
|
+
except WantWriteError:
|
|
369
|
+
self._flush(connection)
|
|
370
|
+
continue
|
|
371
|
+
self._flush(connection)
|
|
372
|
+
return result
|
|
373
|
+
|
|
374
|
+
def _flush(self, connection: Connected) -> None:
|
|
375
|
+
while self._unsent or connection.buffer.outgoing_bytes_buffered():
|
|
376
|
+
if not self._unsent:
|
|
377
|
+
self._unsent = connection.buffer.process_outgoing(RECEIVE_SIZE)
|
|
378
|
+
try:
|
|
379
|
+
sent = connection.transport.send(self._unsent)
|
|
380
|
+
except BlockingIOError:
|
|
381
|
+
raise WantWriteError("the transport is not ready for writing") from None
|
|
382
|
+
self._unsent = self._unsent[sent:]
|
|
383
|
+
|
|
384
|
+
def _fill(self, connection: Connected) -> None:
|
|
385
|
+
try:
|
|
386
|
+
data = connection.transport.recv(RECEIVE_SIZE)
|
|
387
|
+
except BlockingIOError:
|
|
388
|
+
raise WantReadError("the transport has no data yet") from None
|
|
389
|
+
if not data:
|
|
390
|
+
raise RaggedEOF("the transport closed before close_notify")
|
|
391
|
+
connection.buffer.process_incoming(data)
|
|
392
|
+
|
|
393
|
+
def _shutdown_once(self, connection: Connected) -> None:
|
|
394
|
+
try:
|
|
395
|
+
connection.buffer.shutdown()
|
|
396
|
+
except WantReadError:
|
|
397
|
+
pass
|
|
398
|
+
self._flush(connection)
|