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/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)