Skip to content

Commit 576d872

Browse files
committed
Align SSL adapters by removing bind()
Relocated `SSLConnection` instantiation from `bind()` to `wrap()` to align with the adapter interface where `wrap()` is responsible for converting raw sockets to SSL connections. With this chenge `bind()` serves no purpose and so is deprecated. This change also fixes the pyOpenSSL adapter so that it doesn't prematurely wrap the raw socket, an issue in the old `bind()` implementation.
1 parent a475500 commit 576d872

File tree

11 files changed

+170
-45
lines changed

11 files changed

+170
-45
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ per-file-ignores =
128128
cheroot/errors.py: DAR101, DAR201, I003, RST304, WPS111, WPS121, WPS422
129129
cheroot/makefile.py: DAR101, DAR201, DAR401, E800, I003, I004, N801, N802, S101, WPS100, WPS110, WPS111, WPS117, WPS120, WPS121, WPS122, WPS123, WPS130, WPS204, WPS210, WPS212, WPS213, WPS220, WPS229, WPS231, WPS232, WPS338, WPS420, WPS422, WPS429, WPS431, WPS504, WPS604, WPS606
130130
cheroot/server.py: DAR003, DAR101, DAR201, DAR202, DAR301, DAR401, E800, I001, I003, I004, I005, N806, RST201, RST301, RST303, RST304, WPS100, WPS110, WPS111, WPS115, WPS120, WPS121, WPS122, WPS130, WPS132, WPS201, WPS202, WPS204, WPS210, WPS211, WPS212, WPS213, WPS214, WPS220, WPS221, WPS225, WPS226, WPS229, WPS230, WPS231, WPS236, WPS237, WPS238, WPS301, WPS338, WPS342, WPS410, WPS420, WPS421, WPS422, WPS429, WPS432, WPS504, WPS505, WPS601, WPS602, WPS608, WPS617
131-
cheroot/ssl/builtin.py: DAR101, DAR201, DAR401, I001, I003, N806, RST304, WPS110, WPS111, WPS115, WPS117, WPS120, WPS121, WPS122, WPS130, WPS201, WPS210, WPS214, WPS229, WPS231, WPS338, WPS422, WPS501, WPS505, WPS529, WPS608, WPS612
131+
cheroot/ssl/builtin.py: DAR101, DAR201, DAR401, I001, I003, N806, RST304, WPS110, WPS111, WPS115, WPS117, WPS120, WPS121, WPS122, WPS130, WPS201, WPS210, WPS214, WPS229, WPS231, WPS338, WPS422, WPS501, WPS505, WPS529, WPS608
132132
cheroot/ssl/pyopenssl.py: C815, DAR101, DAR201, DAR401, I001, I003, I005, N801, N804, RST304, WPS100, WPS110, WPS111, WPS117, WPS120, WPS121, WPS130, WPS210, WPS220, WPS221, WPS225, WPS229, WPS231, WPS238, WPS301, WPS335, WPS338, WPS420, WPS422, WPS430, WPS432, WPS501, WPS504, WPS505, WPS601, WPS608, WPS615
133133
cheroot/test/conftest.py: DAR101, DAR201, DAR301, I001, I003, I005, WPS100, WPS130, WPS325, WPS354, WPS420, WPS422, WPS430, WPS457
134134
cheroot/test/helper.py: DAR101, DAR201, DAR401, I001, I003, I004, N802, WPS110, WPS111, WPS121, WPS201, WPS220, WPS231, WPS301, WPS414, WPS421, WPS422, WPS505

cheroot/server.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import traceback as traceback_
8181
import urllib.parse
8282
from functools import lru_cache
83+
from warnings import warn as _warn
8384

8485
from . import __version__, connections, errors
8586
from ._compat import IS_PPC, bton
@@ -1369,6 +1370,7 @@ def _handle_no_ssl(self, req):
13691370
'this server only speaks HTTPS on this port.'
13701371
)
13711372
req.simple_response('400 Bad Request', msg)
1373+
self.wfile.flush()
13721374
self.linger = True
13731375

13741376
def _conditional_error(self, req, response):
@@ -1987,7 +1989,9 @@ def bind(self, family, type, proto=0):
19871989
type,
19881990
proto,
19891991
self.nodelay,
1990-
self.ssl_adapter,
1992+
# ssl_adapter is passed to HTTPServer, not needed here
1993+
# this parameter is deprecated and will be removed in future
1994+
None,
19911995
self.reuse_port,
19921996
)
19931997
sock = self.socket = self.bind_socket(sock, self.bind_addr)
@@ -2115,7 +2119,25 @@ def prepare_socket( # pylint: disable=too-many-positional-arguments
21152119
ssl_adapter,
21162120
reuse_port=False,
21172121
):
2118-
"""Create and prepare the socket object."""
2122+
"""
2123+
Create and prepare the socket object.
2124+
2125+
:param ssl_adapter: Legacy SSL adapter parameter.
2126+
This argument is now ignored internally.
2127+
2128+
:deprecated ssl_adapter: This parameter is now deprecated and will be
2129+
removed in a future release. Pass the adapter to the 'HTTPServer`
2130+
constructor instead.
2131+
"""
2132+
if ssl_adapter is not None:
2133+
_warn(
2134+
'The `ssl_adapter` parameter in `prepare_socket` is deprecated '
2135+
'and will be removed in a future version. Pass the adapter'
2136+
' to the `HTTPServer` constructor instead.',
2137+
DeprecationWarning,
2138+
stacklevel=2,
2139+
)
2140+
21192141
sock = socket.socket(family, type, proto)
21202142
connections.prevent_socket_inheritance(sock)
21212143

@@ -2140,9 +2162,6 @@ def prepare_socket( # pylint: disable=too-many-positional-arguments
21402162
if nodelay and not isinstance(bind_addr, (str, bytes)):
21412163
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
21422164

2143-
if ssl_adapter is not None:
2144-
sock = ssl_adapter.bind(sock)
2145-
21462165
# If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
21472166
# activate dual-stack. See
21482167
# https://github.com/cherrypy/cherrypy/issues/871.

cheroot/ssl/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Implementation of the SSL adapter base interface."""
22

33
from abc import ABC, abstractmethod
4+
from warnings import warn as _warn
45

56

67
class Adapter(ABC):
@@ -31,14 +32,25 @@ def __init__(
3132
self.private_key_password = private_key_password
3233
self.context = None
3334

34-
@abstractmethod
3535
def bind(self, sock):
36-
"""Wrap and return the given socket."""
36+
"""
37+
Return the given socket.
38+
39+
Deprecated:
40+
This method no longer performs any SSL-specific operations.
41+
SSL wrapping now happens in :meth:`.wrap`. :meth:`.bind` will be
42+
removed in a future version.
43+
"""
44+
_warn(
45+
'SSLAdapter.bind() is deprecated and will be removed in a future version.',
46+
DeprecationWarning,
47+
stacklevel=2,
48+
)
3749
return sock
3850

3951
@abstractmethod
4052
def wrap(self, sock):
41-
"""Wrap and return the given socket, plus WSGI environ entries."""
53+
"""Wrap the given socket and return WSGI environ entries."""
4254
raise NotImplementedError # pragma: no cover
4355

4456
@abstractmethod

cheroot/ssl/__init__.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class Adapter(ABC):
1818
*,
1919
private_key_password: str | bytes | None = ...,
2020
): ...
21-
@abstractmethod
2221
def bind(self, sock): ...
2322
@abstractmethod
2423
def wrap(self, sock): ...

cheroot/ssl/builtin.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,10 +303,6 @@ def context(self, context):
303303
if ssl.HAS_SNI and context.sni_callback is None:
304304
context.sni_callback = _sni_callback
305305

306-
def bind(self, sock):
307-
"""Wrap and return the given socket."""
308-
return super(BuiltinSSLAdapter, self).bind(sock)
309-
310306
def wrap(self, sock):
311307
"""Wrap and return the given socket, plus WSGI environ entries."""
312308
try:

cheroot/ssl/builtin.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ class BuiltinSSLAdapter(Adapter):
2020
def context(self): ...
2121
@context.setter
2222
def context(self, context) -> None: ...
23-
def bind(self, sock): ...
2423
def wrap(self, sock): ...
2524
def get_environ(self, sock): ...
2625
def makefile(self, sock, mode: str = ..., bufsize: int = ...): ...

cheroot/ssl/pyopenssl.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ def __new__(mcl, name, bases, nmspc):
216216
'settimeout',
217217
'gettimeout',
218218
'shutdown',
219+
'recv_into',
220+
'_decref_socketios',
219221
)
220222
proxy_methods_no_args = ('shutdown',)
221223

@@ -270,6 +272,17 @@ def __init__(self, *args):
270272
self._ssl_conn = SSL.Connection(*args)
271273
self._lock = threading.RLock()
272274

275+
@property
276+
def _socket(self):
277+
"""
278+
Expose underlying raw socket.
279+
280+
This is needed for times when the cheroot server needs access to the
281+
original socket object, e.g. in response to a client attempting
282+
to speak plain HTTP on an HTTPS port.
283+
"""
284+
return self._ssl_conn._socket
285+
273286

274287
class pyOpenSSLAdapter(Adapter):
275288
"""A wrapper for integrating :doc:`pyOpenSSL <pyopenssl:index>`."""
@@ -318,22 +331,18 @@ def __init__(
318331
private_key_password=private_key_password,
319332
)
320333

321-
self._environ = None
322-
323-
def bind(self, sock):
324-
"""Wrap and return the given socket."""
325-
if self.context is None:
326-
self.context = self.get_context()
327-
conn = SSLConnection(self.context, sock)
334+
self.context = self.get_context()
328335
self._environ = self.get_environ()
329-
return conn
330336

331337
def wrap(self, sock):
332338
"""Wrap and return the given socket, plus WSGI environ entries."""
333339
# pyOpenSSL doesn't perform the handshake until the first read/write
334340
# forcing the handshake to complete tends to result in the connection
335341
# closing so we can't reliably access protocol/client cert for the env
336-
return sock, self._environ.copy()
342+
conn = SSLConnection(self.context, sock)
343+
344+
conn.set_accept_state() # Tell OpenSSL this is a server connection
345+
return conn, self._environ.copy()
337346

338347
def _password_callback(
339348
self,
@@ -442,7 +451,8 @@ def makefile(self, sock, mode='r', bufsize=-1):
442451
if 'r' in mode
443452
else SSLFileobjectStreamWriter
444453
)
445-
if SSL and isinstance(sock, ssl_conn_type):
454+
# sock is an pyopenSSL.SSLConnection instance here
455+
if SSL and isinstance(sock, SSLConnection):
446456
wrapped_socket = cls(sock, mode, bufsize)
447457
wrapped_socket.ssl_timeout = sock.gettimeout()
448458
return wrapped_socket

cheroot/ssl/pyopenssl.pyi

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ class pyOpenSSLAdapter(Adapter):
3434
*,
3535
private_key_password: str | bytes | None = ...,
3636
) -> None: ...
37-
def bind(self, sock): ...
3837
def wrap(self, sock): ...
3938
def _password_callback(
4039
self,

cheroot/test/test_ssl.py

Lines changed: 100 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import http.client
66
import json
77
import os
8+
import socket
89
import ssl
910
import subprocess
1011
import sys
@@ -25,6 +26,8 @@
2526
load_pem_private_key,
2627
)
2728

29+
from cheroot.ssl import Adapter
30+
2831
from .._compat import (
2932
IS_ABOVE_OPENSSL10,
3033
IS_ABOVE_OPENSSL31,
@@ -39,7 +42,7 @@
3942
ntob,
4043
ntou,
4144
)
42-
from ..server import Gateway, HTTPServer, get_ssl_adapter_class
45+
from ..server import HTTPServer, get_ssl_adapter_class
4346
from ..testing import (
4447
ANY_INTERFACE_IPV4,
4548
ANY_INTERFACE_IPV6,
@@ -758,8 +761,6 @@ def test_http_over_https_error(
758761
tls_certificate_chain_pem_path,
759762
tls_certificate_private_key_pem_path,
760763
)
761-
if adapter_type == 'pyopenssl':
762-
tls_adapter.context = tls_adapter.get_context()
763764

764765
tls_certificate.configure_cert(tls_adapter.context)
765766

@@ -908,28 +909,108 @@ def test_openssl_adapter_with_false_key_password(
908909
expected_warn,
909910
):
910911
"""Check that server init fails when wrong private key password given."""
911-
httpserver = HTTPServer(
912-
bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
913-
gateway=Gateway,
914-
)
915-
916912
tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)
917-
tls_adapter = tls_adapter_cls(
918-
certificate=tls_certificate_chain_pem_path,
919-
private_key=tls_certificate_passwd_private_key_pem_path,
920-
private_key_password=false_password,
921-
)
922-
923-
httpserver.ssl_adapter = tls_adapter
924-
925913
with expected_warn, pytest.raises(
926914
OpenSSL.SSL.Error,
927915
# Decode error has happened very rarely with Python 3.9 in MacOS.
928916
# Might be caused by a random issue in file handling leading
929917
# to interpretation of garbage characters in certificates.
930918
match=r'.+\'(bad decrypt|decode error)\'.+',
931919
):
932-
httpserver.prepare()
920+
tls_adapter_cls(
921+
certificate=tls_certificate_chain_pem_path,
922+
private_key=tls_certificate_passwd_private_key_pem_path,
923+
private_key_password=false_password,
924+
)
925+
926+
927+
@pytest.fixture
928+
def dummy_adapter(monkeypatch):
929+
"""Provide a dummy SSL adapter instance."""
930+
# hide abstract methods so we can instantiate Adapter
931+
monkeypatch.setattr(Adapter, '__abstractmethods__', set())
932+
# pylint: disable=abstract-class-instantiated
933+
return Adapter(
934+
certificate='cert.pem',
935+
private_key='key.pem',
936+
)
937+
938+
939+
def test_bind_deprecated_call(dummy_adapter):
940+
"""Test deprecated ``bind()`` method issues warning and returns socket."""
941+
sock = socket.socket()
942+
943+
with pytest.deprecated_call():
944+
result = dummy_adapter.bind(sock)
945+
946+
assert result is sock
947+
948+
sock.close()
949+
950+
951+
def test_prepare_socket_emits_deprecation_warning(
952+
dummy_adapter,
953+
):
954+
"""
955+
Test ``prepare_socket()`` deprecated argument triggers a warning.
956+
957+
``ssl_adapter`` has been deprecated in ``prepare_socket()``.
958+
"""
959+
# Required parameters for prepare_socket (standard IPv4 TCP config)
960+
bind_addr = ('127.0.0.1', 8080)
961+
family = socket.AF_INET
962+
sock_type = socket.SOCK_STREAM
963+
proto = socket.IPPROTO_TCP
964+
nodelay = True
965+
966+
with pytest.deprecated_call():
967+
sock = HTTPServer.prepare_socket(
968+
bind_addr=bind_addr,
969+
family=family,
970+
type=sock_type,
971+
proto=proto,
972+
nodelay=nodelay,
973+
ssl_adapter=dummy_adapter,
974+
)
975+
976+
# Check that the returned object is indeed a socket
977+
assert isinstance(sock, socket.socket)
978+
# Check we have a socket configured with file descriptor
979+
assert sock.fileno() > 0
980+
981+
sock.close()
982+
983+
984+
def test_prepare_socket_no_warning(recwarn):
985+
"""Test no warning is emitted when 'ssl_adapter' is correctly omitted."""
986+
# Required parameters for prepare_socket (standard IPv4 TCP config)
987+
bind_addr = ('127.0.0.1', 8080)
988+
family = socket.AF_INET
989+
sock_type = socket.SOCK_STREAM
990+
proto = socket.IPPROTO_TCP
991+
nodelay = True
992+
993+
# The ptyest 'recwarn' fixture automatically
994+
# collects warnings during this block.
995+
# with contextlib.suppress(socket.error):
996+
sock = HTTPServer.prepare_socket(
997+
bind_addr=bind_addr,
998+
family=family,
999+
type=sock_type,
1000+
proto=proto,
1001+
nodelay=nodelay,
1002+
ssl_adapter=None,
1003+
)
1004+
1005+
# Assert that no DeprecationWarning was recorded by checking the list
1006+
# collected by the recwarn fixture.
1007+
deprecation_warnings = [
1008+
w for w in recwarn.list if issubclass(w.category, DeprecationWarning)
1009+
]
1010+
assert len(deprecation_warnings) == 0
9331011

934-
assert not httpserver.requests._threads
935-
assert not httpserver.ready
1012+
# Check that the returned object is indeed a socket
1013+
assert isinstance(sock, socket.socket)
1014+
# Check we have a socket configured with file descriptor
1015+
assert sock.fileno() > 0
1016+
sock.close()
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Deprecated :py:meth:`~cheroot.ssl.Adapter.bind` method from the :py:class:`~cheroot.ssl.Adapter`
2+
interface and respective implementations. Previously :py:meth:`~cheroot.ssl.Adapter.bind` was
3+
doing nothing in the `:py:class:`builtin TLS adapter <cheroot.ssl.builtin.BuiltinSSLAdapter>`
4+
but was being used in the :py:class:`~cheroot.ssl.pyopenssl.pyOpenSSLAdapter` to wrap the socket.
5+
Socket wrapping is now done exclusively in the :py:meth:`~cheroot.ssl.Adapter.wrap`
6+
method subclass implementations. A side-effect of this change is that the ``ssl_adapter`` argument
7+
of :py:meth:`~cheroot.server.HTTPServer.prepare_socket()` is also deprecated.
8+
9+
-- by :user:`julianz-`

0 commit comments

Comments
 (0)