Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions cheroot/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
125 changes: 38 additions & 87 deletions cheroot/ssl/pyopenssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@

import socket
import sys
import threading
import time
from warnings import warn as _warn

Expand Down Expand Up @@ -176,99 +175,51 @@ 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_)
class SSLConnection(SSL.Connection):
"""
A compatibility wrapper around :py:class:`OpenSSL.SSL.Connection`.

proxy_prop_wrapper.__name__ = property_
return property(proxy_prop_wrapper)
This class exists primarily to ensure the standard Python socket method
:py:meth:`.shutdown()` is available for interface compatibility.
"""

for p in proxy_props:
nmspc[p] = make_property(p)
def makefile(self, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? Can we replicate the method signature exactly?

Copy link
Contributor Author

@julianz- julianz- Nov 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makefile() I added because the linter complained about makefile being abstract and needing an implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which linter? Do you have logs?

"""
Raise :exc:`NotImplementedError` for ``makefile()`` interface.

# Doesn't work via super() for some reason.
# Falling back to type() instead:
return type(name, bases, nmspc)
PyOpenSSL's :py:class:`~OpenSSL.SSL.Connection` marks
:py:meth:`~OpenSSL.SSL.Connection.makefile` as not implemented.
This method exists only for interface compatibility with
Python sockets.
"""
# Replicate the parent's behavior to avoid calling a method
# that is known to be unimplemented.
raise NotImplementedError(
"PyOpenSSL's Connection class does not support the makefile() interface",
)

def shutdown(self, how=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should have something for sock_shutdown().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike makefile() which I just had to add, the linter is not complaining about this method being missing. We could add for socket compatibility I guess, but it's just going to call the shutdown method. If we try to replicate the whole socket interface then wouldn't we need to add a bunch of other methods? Then we end up adding all kinds of cruft?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm mostly curious about the calling code using it rather than us reimplementing it. This class will get the implementation through inheritance.

@julianz- I just remembered that the calling code actually invokes sock_shutdown() and not shutdown(). See

'sock_shutdown',
.

I wonder if we could have interface that would be uniform across adapters and wouldn't have that ugly getattr() hack, which is effectively an abstraction leak.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe then we assume all connection adapters (including SSLConnection) implement sock_shutdown?

"""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) \
<pyopenssl:OpenSSL.SSL.Connection>`
"""
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):
Expand Down
10 changes: 6 additions & 4 deletions cheroot/ssl/pyopenssl.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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__(
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog-fragments.d/787.misc.rst
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm actually not sure if this constitutes a breaking change. SemVer talks about breaking changes in the context of API compatibility and the API seems to have remained intact. WDYT?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the API offered thread-safety and now suddenly we are saying these methods are not thread-safe that sounds like a breaking change to me, no? It's somewhat moot I guess if, in practice, no-one needs these methods to be thread-safe but just in case it's better to clear this is a potentially breaking change. If not a breaking change what category would be best?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I'm torn. We could keep it breaking. Otherwise, it'd probably be misc or something like that, which is not very prominent in the change log.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure. That makes sense. I'm fine to move to misc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@julianz- I'm not opposed to keeping it breaking. Just thinking out loud. If it's breaking, I'd bump the major version component on release. Otherwise, it might be a minor change.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is Breaking Bad? haha. i'm torn also. i changed to misc but if you want to go back to breaking let me know.

Original file line number Diff line number Diff line change
@@ -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-`
2 changes: 2 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ positionally
pre
preconfigure
py
pyOpenSSL
pytest
pythonic
readonly
Expand Down Expand Up @@ -75,6 +76,7 @@ tuples
unbuffered
unclosed
unfilterable
unhandled
unregister
unregisters
uptime
Expand Down
37 changes: 0 additions & 37 deletions stubtest_allowlist.txt
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading