Skip to content

Commit 2c8e1a1

Browse files
committed
Introduce TLSSocket abstraction for uniform handling
Introduces a new `TLSSocket` class to act as a unified wrapper for SSL/TLS connections, regardless of the underlying adapter (`builtin`, `pyOpenSSL`). This refactoring aims to: 1. Simplify adapter logic by centralizing common TLS socket properties and methods (e.g., cipher details, certificate paths). 2. Improve consistency when populating WSGI environment variables. 3. Centralize error handling in the adapters.
1 parent a475500 commit 2c8e1a1

19 files changed

+2731
-742
lines changed

.flake8

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,14 @@ per-file-ignores =
124124
cheroot/__main__.py: WPS130
125125
cheroot/_compat.py: DAR101, DAR201, DAR301, DAR401, I003, RST304, WPS100, WPS111, WPS123, WPS226, WPS229, WPS420, WPS422, WPS432, WPS504, WPS505
126126
cheroot/cli.py: DAR101, DAR201, DAR401, I001, I004, I005, WPS100, WPS110, WPS120, WPS130, WPS202, WPS226, WPS229, WPS338, WPS420, WPS421
127-
cheroot/connections.py: DAR101, DAR201, DAR301, DAR401, I001, I003, I004, I005, RST304, S104, WPS100, WPS110, WPS111, WPS121, WPS122, WPS130, WPS201, WPS204, WPS210, WPS212, WPS214, WPS220, WPS229, WPS231, WPS301, WPS324, WPS338, WPS420, WPS421, WPS422, WPS432, WPS501, WPS504, WPS505
127+
cheroot/connections.py: DAR101, DAR201, DAR301, DAR401, I001, I003, I004, I005, RST304, S104, WPS100, WPS110, WPS111, WPS121, WPS122, WPS130, WPS201, WPS204, WPS210, WPS212, WPS214, WPS220, WPS229, WPS231, WPS237, WPS301, WPS324, WPS338, WPS420, WPS421, WPS422, WPS432, WPS501, WPS504, WPS505
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, WPS204, WPS210, WPS220, WPS214, WPS226, WPS229, WPS231, WPS338, WPS421, WPS422, WPS501, WPS505, WPS529, WPS608, WPS612
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
133-
cheroot/test/conftest.py: DAR101, DAR201, DAR301, I001, I003, I005, WPS100, WPS130, WPS325, WPS354, WPS420, WPS422, WPS430, WPS457
133+
cheroot/ssl/tls_socket.py: DAR101, DAR201, DAR401, WPS110, WPS122, WPS210, WPS212, WPS214, WPS220, WPS225, WPS226, WPS229, WPS231, WPS238, WPS338, WPS362, WPS407
134+
cheroot/test/conftest.py: DAR101, DAR201, DAR301, I001, I003, I005, WPS100, WPS130, WPS202, WPS325, WPS354, WPS420, WPS422, WPS430, WPS457
134135
cheroot/test/helper.py: DAR101, DAR201, DAR401, I001, I003, I004, N802, WPS110, WPS111, WPS121, WPS201, WPS220, WPS231, WPS301, WPS414, WPS421, WPS422, WPS505
135136
cheroot/test/test_cli.py: DAR101, DAR201, I001, I005, N802, S101, S108, WPS110, WPS421, WPS431, WPS473
136137
cheroot/test/test_makefile.py: DAR101, DAR201, I004, RST304, S101, WPS110, WPS122
@@ -144,7 +145,9 @@ per-file-ignores =
144145
cheroot/testing.py: C815, DAR101, DAR201, DAR301, I001, I003, S104, WPS100, WPS202, WPS211, WPS229, WPS301, WPS414, WPS420, WPS422, WPS430
145146
cheroot/workers/threadpool.py: DAR101, DAR201, E800, I001, I003, I004, RST201, RST203, RST301, WPS100, WPS110, WPS111, WPS121, WPS122, WPS210, WPS211, WPS214, WPS220, WPS229, WPS230, WPS231, WPS335, WPS338, WPS362, WPS363, WPS410, WPS414, WPS420, WPS422, WPS432, WPS501, WPS505, WPS601, WPS602, WPS617
146147
cheroot/wsgi.py: DAR101, DAR201, DAR401, I001, I003, I005, N801, RST201, RST301, WPS100, WPS110, WPS111, WPS114, WPS121, WPS122, WPS130, WPS210, WPS211, WPS226, WPS229, WPS231, WPS338, WPS420, WPS421, WPS422, WPS430, WPS501, WPS504, WPS602, WPS608
147-
cheroot/ssl/__init__.py: DAR101, DAR201, I003, WPS412, WPS422
148+
cheroot/ssl/__init__.py: DAR101, DAR201, I003, WPS210, WPS412, WPS422
149+
cheroot/test/ssl/test_ssl_builtin.py: DAR101, DAR201, I003, WPS118, WPS201, WPS202, WPS210, WPS213, WPS218, WPS211, WPS226, WPS229, WPS231, WPS243, WPS412, WPS420, WPS422, WPS430, WPS505
150+
cheroot/test/ssl/test_ssl_pyopenssl.py: DAR101, DAR201, I003, WPS118, WPS201, WPS202, WPS204, WPS210, WPS220, WPS213, WPS218, WPS211, WPS226, WPS229, WPS231, WPS243, WPS412, WPS420, WPS422, WPS430, WPS432, WPS435, WPS505
148151
cheroot/test/_pytest_plugin.py: DAR101, I003, I004, WPS422
149152
cheroot/test/test__compat.py: DAR101, I001, I003, I005, WPS116, WPS226, WPS422, S101
150153
cheroot/test/test_errors.py: DAR101, WPS509, S101

.mypy.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[mypy]
2-
python_version = 3.8
2+
python_version = 3.9
33
color_output = true
44
error_summary = true
55
files =

cheroot/connections.py

Lines changed: 46 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""Utilities to manage open connections."""
22

3-
import io
43
import os
54
import selectors
65
import socket
@@ -295,8 +294,17 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
295294

296295
mf = MakeFile
297296
ssl_env = {}
297+
298298
# if ssl cert and key are set, we try to be a secure HTTP server
299299
if self.server.ssl_adapter is not None:
300+
# 1. Preemptive check for BuiltinSSLAdapter
301+
if hasattr(
302+
self.server.ssl_adapter,
303+
'_check_for_plain_http',
304+
) and self.server.ssl_adapter._check_for_plain_http(s):
305+
return self._send_bad_request_plain_http_error(s, addr)
306+
307+
# 2. Secure HTTP server attempt
300308
try:
301309
s, ssl_env = self.server.ssl_adapter.wrap(s)
302310
except errors.FatalSSLAlert as tls_connection_drop_error:
@@ -306,35 +314,11 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
306314
f'{tls_connection_drop_error!s}',
307315
)
308316
return None
309-
except errors.NoSSLError as http_over_https_err:
310-
self.server.error_log(
311-
f'Client {addr!s} attempted to speak plain HTTP into '
312-
'a TCP connection configured for TLS-only traffic — '
313-
'trying to send back a plain HTTP error response: '
314-
f'{http_over_https_err!s}',
315-
)
316-
msg = (
317-
'The client sent a plain HTTP request, but '
318-
'this server only speaks HTTPS on this port.'
319-
)
320-
buf = [
321-
'%s 400 Bad Request\r\n' % self.server.protocol,
322-
'Content-Length: %s\r\n' % len(msg),
323-
'Content-Type: text/plain\r\n\r\n',
324-
msg,
325-
]
326-
327-
wfile = mf(s, 'wb', io.DEFAULT_BUFFER_SIZE)
328-
try:
329-
wfile.write(''.join(buf).encode('ISO-8859-1'))
330-
except OSError as ex:
331-
if ex.args[0] not in errors.socket_errors_to_ignore:
332-
raise
333-
return None
334-
mf = self.server.ssl_adapter.makefile
335-
# Re-apply our timeout since we may have a new socket object
336-
if hasattr(s, 'settimeout'):
337-
s.settimeout(self.server.timeout)
317+
except errors.NoSSLError:
318+
# this only arises for PyOpenSSL as
319+
# we handle http over https pre-emptivley
320+
# for the Builtin adapter
321+
return self._send_bad_request_plain_http_error(s, addr)
338322

339323
conn = self.server.ConnectionClass(self.server, s, mf)
340324

@@ -381,6 +365,38 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
381365
return None
382366
raise
383367

368+
def _send_bad_request_plain_http_error(self, sock, addr):
369+
"""Send Bad Request 400 response, and close the socket."""
370+
self.server.error_log(
371+
f'Client {addr!s} attempted to speak plain HTTP into '
372+
'a TCP connection configured for TLS-only traffic — '
373+
'Sending 400 Bad Request.',
374+
)
375+
376+
msg = (
377+
'The client sent a plain HTTP request, but this server '
378+
'only speaks HTTPS on this port.'
379+
)
380+
381+
response_parts = [
382+
f'{self.server.protocol} 400 Bad Request\r\n',
383+
'Content-Type: text/plain\r\n',
384+
f'Content-Length: {len(msg)}\r\n',
385+
'Connection: close\r\n',
386+
'\r\n',
387+
msg,
388+
]
389+
response_bytes = ''.join(response_parts).encode('ISO-8859-1')
390+
391+
try:
392+
sock.sendall(response_bytes)
393+
sock.shutdown(socket.SHUT_WR)
394+
except OSError as ex:
395+
if ex.args[0] not in errors.socket_errors_to_ignore:
396+
raise
397+
398+
sock.close()
399+
384400
def close(self):
385401
"""Close all monitored connections."""
386402
for _, conn in self._selector.connections:

cheroot/makefile.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import _pyio as io
55
import socket
66

7+
from .ssl.tls_socket import TLSSocket
8+
79

810
# Write only 16K at a time to sockets
911
SOCK_WRITE_BLOCKSIZE = 16384
@@ -40,7 +42,14 @@ class StreamReader(io.BufferedReader):
4042

4143
def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
4244
"""Initialize socket stream reader."""
43-
super().__init__(socket.SocketIO(sock, mode), bufsize)
45+
# If sock is TLSSocket, it's already wrapped.
46+
if isinstance(sock, TLSSocket):
47+
raw_io = sock
48+
else:
49+
# Standard socket - wrap with SocketIO
50+
raw_io = socket.SocketIO(sock, mode)
51+
52+
super().__init__(raw_io, bufsize)
4453
self.bytes_read = 0
4554

4655
def read(self, *args, **kwargs):
@@ -59,7 +68,14 @@ class StreamWriter(BufferedWriter):
5968

6069
def __init__(self, sock, mode='w', bufsize=io.DEFAULT_BUFFER_SIZE):
6170
"""Initialize socket stream writer."""
62-
super().__init__(socket.SocketIO(sock, mode), bufsize)
71+
# If sock is TLSSocket, use it directly as raw I/O
72+
if isinstance(sock, TLSSocket):
73+
raw_io = sock
74+
else:
75+
# Standard socket - wrap with SocketIO
76+
raw_io = socket.SocketIO(sock, mode)
77+
78+
super().__init__(raw_io, bufsize)
6379
self.bytes_written = 0
6480

6581
def write(self, val, *args, **kwargs):

cheroot/server.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1522,7 +1522,7 @@ def _close_kernel_socket(self):
15221522
raise
15231523

15241524

1525-
class HTTPServer:
1525+
class HTTPServer: # noqa: PLR0904
15261526
"""An HTTP server."""
15271527

15281528
_bind_addr = '127.0.0.1'
@@ -1590,14 +1590,26 @@ class HTTPServer:
15901590
ConnectionClass = HTTPConnection
15911591
"""The class to use for handling HTTP connections."""
15921592

1593-
ssl_adapter = None
1593+
_ssl_adapter = None
15941594
"""An instance of ``ssl.Adapter`` (or a subclass).
15951595
15961596
Ref: :py:class:`ssl.Adapter <cheroot.ssl.Adapter>`.
15971597
15981598
You must have the corresponding TLS driver library installed.
15991599
"""
16001600

1601+
@property
1602+
def ssl_adapter(self):
1603+
"""An instance of ssl.Adapter (or a subclass)."""
1604+
return self._ssl_adapter
1605+
1606+
@ssl_adapter.setter
1607+
def ssl_adapter(self, value):
1608+
"""Set the SSL adapter and establish bidirectional reference."""
1609+
self._ssl_adapter = value
1610+
if value is not None:
1611+
value.server = self
1612+
16011613
peercreds_enabled = False
16021614
"""
16031615
Whether :py:data:`True`, peer credentials will be looked up via UNIX

0 commit comments

Comments
 (0)