diff --git a/cheroot/server.py b/cheroot/server.py index b2e83eb28c..8f1f9822ad 100644 --- a/cheroot/server.py +++ b/cheroot/server.py @@ -1537,12 +1537,7 @@ def peer_group(self): def _close_kernel_socket(self): """Terminate the connection at the transport level.""" - # Honor ``sock_shutdown`` for PyOpenSSL connections. - shutdown = getattr( - self.socket, - 'sock_shutdown', - self.socket.shutdown, - ) + shutdown = self.socket.sock_shutdown try: shutdown(socket.SHUT_RDWR) # actually send a TCP FIN diff --git a/cheroot/ssl/pyopenssl.py b/cheroot/ssl/pyopenssl.py index 8a041a79b8..1f0070fa59 100644 --- a/cheroot/ssl/pyopenssl.py +++ b/cheroot/ssl/pyopenssl.py @@ -52,7 +52,6 @@ import socket import sys -import threading import time from warnings import warn as _warn @@ -176,99 +175,36 @@ class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): """SSL file object attached to a socket object.""" -class SSLConnectionProxyMeta: - """Metaclass for generating a bunch of proxy methods.""" - - def __new__(mcl, name, bases, nmspc): - """Attach a list of proxy methods to a new class.""" - proxy_methods = ( - 'get_context', - 'pending', - 'send', - 'write', - 'recv', - 'read', - 'renegotiate', - 'bind', - 'listen', - 'connect', - 'accept', - 'setblocking', - 'fileno', - 'close', - 'get_cipher_list', - 'getpeername', - 'getsockname', - 'getsockopt', - 'setsockopt', - 'makefile', - 'get_app_data', - 'set_app_data', - 'state_string', - 'sock_shutdown', - 'get_peer_certificate', - 'want_read', - 'want_write', - 'set_connect_state', - 'set_accept_state', - 'connect_ex', - 'sendall', - 'settimeout', - 'gettimeout', - 'shutdown', - ) - proxy_methods_no_args = ('shutdown',) - - proxy_props = ('family',) - - def lock_decorator(method): - """Create a proxy method for a new class.""" - - def proxy_wrapper(self, *args): - self._lock.acquire() - try: - new_args = ( - args[:] if method not in proxy_methods_no_args else [] - ) - return getattr(self._ssl_conn, method)(*new_args) - finally: - self._lock.release() - - return proxy_wrapper - - for m in proxy_methods: - nmspc[m] = lock_decorator(m) - nmspc[m].__name__ = m - - def make_property(property_): - """Create a proxy method for a new class.""" - - def proxy_prop_wrapper(self): - return getattr(self._ssl_conn, property_) - - proxy_prop_wrapper.__name__ = property_ - return property(proxy_prop_wrapper) - - for p in proxy_props: - nmspc[p] = make_property(p) +class SSLConnection(SSL.Connection): + """ + A compatibility wrapper around :py:class:`OpenSSL.SSL.Connection`. - # Doesn't work via super() for some reason. - # Falling back to type() instead: - return type(name, bases, nmspc) + This class exists primarily to ensure the standard Python socket method + :py:meth:`.shutdown()` is available for interface compatibility. + """ + def shutdown(self, how=None): + """Shutdown the SSL connection. -class SSLConnection(metaclass=SSLConnectionProxyMeta): - r"""A thread-safe wrapper for an ``SSL.Connection``. + :param how: Ignored. PyOpenSSL's + :py:meth:`~OpenSSL.SSL.Connection.shutdown` + method does not accept any arguments. + Present here for interface compatibility with Python + :py:meth:`~socket.socket.shutdown` that + :py:class:`ssl.SSLSocket` wrapper exposes. + :type how: object + """ + return super().shutdown() - :param tuple args: the arguments to create the wrapped \ - :py:class:`SSL.Connection(*args) \ - ` - """ + def sock_shutdown(self, how=None): + """Shutdown the SSL connection. - def __init__(self, *args): - """Initialize SSLConnection instance.""" - self._ssl_conn = SSL.Connection(*args) - self._lock = threading.RLock() + This method is provided for interface compatibility and delegates + directly to the standard shutdown() method. + """ + # We call self.shutdown(how) to use the method where we added + # the compatibility logic (handling 'how=None' for the parent). + return self.shutdown(how) class pyOpenSSLAdapter(Adapter): diff --git a/cheroot/ssl/pyopenssl.pyi b/cheroot/ssl/pyopenssl.pyi index 59dae05ae7..9d2c987320 100644 --- a/cheroot/ssl/pyopenssl.pyi +++ b/cheroot/ssl/pyopenssl.pyi @@ -18,11 +18,13 @@ class SSLFileobjectMixin: class SSLFileobjectStreamReader(SSLFileobjectMixin, StreamReader): ... # type:ignore[misc] class SSLFileobjectStreamWriter(SSLFileobjectMixin, StreamWriter): ... # type:ignore[misc] -class SSLConnectionProxyMeta: - def __new__(mcl, name, bases, nmspc): ... +class SSLConnection(SSL.Connection): + proxy_methods: tuple[str, ...] + proxy_methods_no_args: tuple[str, ...] + proxy_props: tuple[str, ...] -class SSLConnection: - def __init__(self, *args) -> None: ... + def shutdown(self, how: int | None = None) -> None: ... # type: ignore[override] + def sock_shutdown(self, how: int | None = None) -> None: ... # type: ignore[override] class pyOpenSSLAdapter(Adapter): def __init__( diff --git a/docs/changelog-fragments.d/787.misc.rst b/docs/changelog-fragments.d/787.misc.rst new file mode 100644 index 0000000000..8400cbb263 --- /dev/null +++ b/docs/changelog-fragments.d/787.misc.rst @@ -0,0 +1,5 @@ +Removed thread-safety locking logic from +:py:class:`cheroot.ssl.pyopenssl.SSLConnection` as +this wrapper did not need to be thread-safe. + +-- by :user:`julianz-` diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 1e1022c82f..7f6f306e56 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -44,6 +44,7 @@ positionally pre preconfigure py +pyOpenSSL pytest pythonic readonly @@ -75,6 +76,7 @@ tuples unbuffered unclosed unfilterable +unhandled unregister unregisters uptime diff --git a/stubtest_allowlist.txt b/stubtest_allowlist.txt index cc6e94a5a5..b2ab351a6a 100644 --- a/stubtest_allowlist.txt +++ b/stubtest_allowlist.txt @@ -1,40 +1,3 @@ -# generated members by metaclass -cheroot.ssl.pyopenssl.SSLConnection.accept -cheroot.ssl.pyopenssl.SSLConnection.bind -cheroot.ssl.pyopenssl.SSLConnection.close -cheroot.ssl.pyopenssl.SSLConnection.connect -cheroot.ssl.pyopenssl.SSLConnection.connect_ex -cheroot.ssl.pyopenssl.SSLConnection.family -cheroot.ssl.pyopenssl.SSLConnection.fileno -cheroot.ssl.pyopenssl.SSLConnection.get_app_data -cheroot.ssl.pyopenssl.SSLConnection.get_cipher_list -cheroot.ssl.pyopenssl.SSLConnection.get_context -cheroot.ssl.pyopenssl.SSLConnection.get_peer_certificate -cheroot.ssl.pyopenssl.SSLConnection.getpeername -cheroot.ssl.pyopenssl.SSLConnection.getsockname -cheroot.ssl.pyopenssl.SSLConnection.getsockopt -cheroot.ssl.pyopenssl.SSLConnection.gettimeout -cheroot.ssl.pyopenssl.SSLConnection.listen -cheroot.ssl.pyopenssl.SSLConnection.makefile -cheroot.ssl.pyopenssl.SSLConnection.pending -cheroot.ssl.pyopenssl.SSLConnection.read -cheroot.ssl.pyopenssl.SSLConnection.recv -cheroot.ssl.pyopenssl.SSLConnection.renegotiate -cheroot.ssl.pyopenssl.SSLConnection.send -cheroot.ssl.pyopenssl.SSLConnection.sendall -cheroot.ssl.pyopenssl.SSLConnection.set_accept_state -cheroot.ssl.pyopenssl.SSLConnection.set_app_data -cheroot.ssl.pyopenssl.SSLConnection.set_connect_state -cheroot.ssl.pyopenssl.SSLConnection.setblocking -cheroot.ssl.pyopenssl.SSLConnection.setsockopt -cheroot.ssl.pyopenssl.SSLConnection.settimeout -cheroot.ssl.pyopenssl.SSLConnection.shutdown -cheroot.ssl.pyopenssl.SSLConnection.sock_shutdown -cheroot.ssl.pyopenssl.SSLConnection.state_string -cheroot.ssl.pyopenssl.SSLConnection.want_read -cheroot.ssl.pyopenssl.SSLConnection.want_write -cheroot.ssl.pyopenssl.SSLConnection.write - # suppress is both a function and class cheroot._compat.suppress