|
54 | 54 | import sys |
55 | 55 | import threading |
56 | 56 | import time |
| 57 | +from contextlib import suppress as _suppress |
57 | 58 | from warnings import warn as _warn |
58 | 59 |
|
59 | 60 |
|
|
74 | 75 | errors, |
75 | 76 | server as cheroot_server, |
76 | 77 | ) |
77 | | -from ..makefile import StreamReader, StreamWriter |
78 | 78 | from . import Adapter |
79 | 79 |
|
80 | 80 |
|
@@ -168,14 +168,6 @@ def send(self, *args, **kwargs): |
168 | 168 | ) |
169 | 169 |
|
170 | 170 |
|
171 | | -class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): |
172 | | - """SSL file object attached to a socket object.""" |
173 | | - |
174 | | - |
175 | | -class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): |
176 | | - """SSL file object attached to a socket object.""" |
177 | | - |
178 | | - |
179 | 171 | class SSLConnectionProxyMeta: |
180 | 172 | """Metaclass for generating a bunch of proxy methods.""" |
181 | 173 |
|
@@ -345,9 +337,11 @@ def wrap(self, sock): |
345 | 337 | ) |
346 | 338 |
|
347 | 339 | conn = SSLConnection(self.context, sock) |
348 | | - |
349 | 340 | conn.set_accept_state() # Tell OpenSSL this is a server connection |
350 | | - return conn, self._environ.copy() |
| 341 | + |
| 342 | + # Wrap the SSLConnection to provide standard socket interface |
| 343 | + tls_socket = TLSSocket(conn) |
| 344 | + return tls_socket, self._environ.copy() |
351 | 345 |
|
352 | 346 | def _password_callback( |
353 | 347 | self, |
@@ -461,17 +455,91 @@ def get_environ(self): |
461 | 455 | return ssl_environ |
462 | 456 |
|
463 | 457 | def makefile(self, sock, mode='r', bufsize=-1): |
464 | | - """Return socket file object.""" |
465 | | - cls = ( |
466 | | - SSLFileobjectStreamReader |
467 | | - if 'r' in mode |
468 | | - else SSLFileobjectStreamWriter |
| 458 | + """ |
| 459 | + Return socket file object. |
| 460 | +
|
| 461 | + ``makefile`` is now deprecated and will be removed in a future |
| 462 | + version. |
| 463 | + """ |
| 464 | + _warn( |
| 465 | + 'The `makefile` method is deprecated and will be removed in a future version. ' |
| 466 | + 'The connection socket should be fully wrapped by the adapter ' |
| 467 | + 'before being passed to the HTTPConnection constructor.', |
| 468 | + DeprecationWarning, |
| 469 | + stacklevel=2, |
469 | 470 | ) |
470 | | - # sock is an pyopenSSL.SSLConnection instance here |
471 | | - if SSL and isinstance(sock, SSLConnection): |
472 | | - wrapped_socket = cls(sock, mode, bufsize) |
473 | | - wrapped_socket.ssl_timeout = sock.gettimeout() |
474 | | - return wrapped_socket |
475 | | - # This is from past: |
476 | | - # TODO: figure out what it's meant for |
477 | | - return cheroot_server.CP_fileobject(sock, mode, bufsize) |
| 471 | + |
| 472 | + return sock.makefile(mode, bufsize) |
| 473 | + |
| 474 | + |
| 475 | +class TLSSocket(SSLFileobjectMixin): |
| 476 | + """ |
| 477 | + Wrap ``pyOpenSSL`` SSL.Connection to work with StreamReader/StreamWriter. |
| 478 | +
|
| 479 | + Handles OpenSSL-specific errors internally so that standard Python I/O |
| 480 | + classes can use it transparently. |
| 481 | + """ |
| 482 | + |
| 483 | + def __init__(self, ssl_conn, ssl_timeout=None, ssl_retry=0.01): |
| 484 | + """Initialize with an SSL.Connection object.""" |
| 485 | + self._ssl_conn = ssl_conn |
| 486 | + self._sock = ssl_conn._socket # Store reference to raw TCP socket |
| 487 | + self._lock = threading.RLock() |
| 488 | + self.ssl_timeout = ssl_timeout or 3.0 |
| 489 | + self.ssl_retry = ssl_retry |
| 490 | + |
| 491 | + # Socket I/O |
| 492 | + # _safe_call is delegated to the SSLFileobjectMixin |
| 493 | + |
| 494 | + def recv_into(self, buffer, nbytes=None): |
| 495 | + """Receive data into a buffer (socket interface).""" |
| 496 | + with self._lock: |
| 497 | + result = self._safe_call( |
| 498 | + True, |
| 499 | + self._ssl_conn.recv_into, |
| 500 | + buffer, |
| 501 | + nbytes, |
| 502 | + ) |
| 503 | + # Handle the case where _safe_call returns b'' for EOF |
| 504 | + if result == b'': |
| 505 | + return 0 |
| 506 | + return result |
| 507 | + |
| 508 | + def send(self, data): |
| 509 | + """Send data (socket interface).""" |
| 510 | + with self._lock: |
| 511 | + return self._safe_call( |
| 512 | + False, # is_reader=False |
| 513 | + self._ssl_conn.send, |
| 514 | + data, |
| 515 | + ) |
| 516 | + |
| 517 | + def fileno(self): |
| 518 | + """Return the file descriptor.""" |
| 519 | + return self._ssl_conn.fileno() |
| 520 | + |
| 521 | + def _decref_socketios(self): |
| 522 | + """Shutdown the connection.""" |
| 523 | + return self._ssl_conn._decref_socketios() |
| 524 | + |
| 525 | + def shutdown(self, how): |
| 526 | + """Shutdown the connection.""" |
| 527 | + # SSL shutdown can fail if the handshake didn't complete or during |
| 528 | + # error conditions. Since shutdown is a cleanup operation, we suppress |
| 529 | + # all errors - the connection will be closed anyway. |
| 530 | + with _suppress(SSL.Error, socket.error, OSError): |
| 531 | + return self._ssl_conn.shutdown(how) |
| 532 | + |
| 533 | + def close(self): |
| 534 | + """Close the TLS socket and underlying connection.""" |
| 535 | + # 1. Attempt an SSL-level shutdown |
| 536 | + with _suppress(SSL.Error, OSError): |
| 537 | + self._ssl_conn.shutdown() |
| 538 | + |
| 539 | + # 2. Close the SSL connection object |
| 540 | + with _suppress(SSL.Error, OSError): |
| 541 | + self._ssl_conn.close() |
| 542 | + |
| 543 | + # 3. Close the raw TCP socket |
| 544 | + with _suppress(OSError): |
| 545 | + self._sock.close() |
0 commit comments