Skip to content

Commit 21b753e

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 21b753e

23 files changed

+2705
-811
lines changed

.flake8

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,17 @@ 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
129-
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
129+
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, WPS226, 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
132-
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
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
132+
cheroot/ssl/pyopenssl.py: C815, DAR101, DAR201, DAR401, I001, I003, I005, N801, N804, RST304, WPS100, WPS110, WPS111, WPS117, WPS120, WPS121, WPS122, WPS130, WPS210, WPS220, WPS221, WPS225, WPS229, WPS231, WPS238, WPS301, WPS335, WPS338, WPS420, WPS422, WPS430, WPS432, WPS501, WPS504, WPS505, WPS601, WPS608, WPS615
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
136-
cheroot/test/test_makefile.py: DAR101, DAR201, I004, RST304, S101, WPS110, WPS122
137+
cheroot/test/test_makefile.py: DAR101, DAR201, I004, RST304, S101, WPS110, WPS122, WPS362
137138
cheroot/test/test_wsgi.py: DAR101, DAR301, I001, I004, S101, WPS110, WPS111, WPS117, WPS118, WPS121, WPS210, WPS421, WPS430, WPS432, WPS441, WPS509
138139
cheroot/test/test_core.py: C815, DAR101, DAR201, DAR401, I003, I004, N805, N806, S101, WPS110, WPS111, WPS114, WPS121, WPS202, WPS204, WPS226, WPS229, WPS324, WPS421, WPS422, WPS432, WPS602
139140
cheroot/test/test_dispatch.py: DAR101, DAR201, S101, WPS111, WPS121, WPS422, WPS430
@@ -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: 44 additions & 37 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
@@ -10,7 +9,6 @@
109

1110
from . import errors
1211
from ._compat import IS_WINDOWS
13-
from .makefile import MakeFile
1412

1513

1614
try:
@@ -293,50 +291,22 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
293291
if hasattr(s, 'settimeout'):
294292
s.settimeout(self.server.timeout)
295293

296-
mf = MakeFile
297294
ssl_env = {}
295+
298296
# if ssl cert and key are set, we try to be a secure HTTP server
299297
if self.server.ssl_adapter is not None:
300298
try:
301299
s, ssl_env = self.server.ssl_adapter.wrap(s)
302-
except errors.FatalSSLAlert as tls_connection_drop_error:
303-
self.server.error_log(
304-
f'Client {addr!s} lost — peer dropped the TLS '
305-
'connection suddenly, during handshake: '
306-
f'{tls_connection_drop_error!s}',
307-
)
308-
return None
309-
except errors.NoSSLError as http_over_https_err:
300+
except errors.FatalSSLAlert as tls_connection_error:
310301
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}',
302+
f'Failed to establish SSL connection with {addr!s}: '
303+
f'{tls_connection_error!s}',
315304
)
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
333305
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)
306+
except errors.NoSSLError:
307+
return self._send_bad_request_plain_http_error(s, addr)
338308

339-
conn = self.server.ConnectionClass(self.server, s, mf)
309+
conn = self.server.ConnectionClass(self.server, s)
340310

341311
if not isinstance(self.server.bind_addr, (str, bytes)):
342312
# optional values
@@ -381,6 +351,43 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
381351
return None
382352
raise
383353

354+
def _send_bad_request_plain_http_error(self, sock, addr):
355+
"""Send Bad Request 400 response, and close the socket."""
356+
self.server.error_log(
357+
f'Client {addr!s} attempted to speak plain HTTP into '
358+
'a TCP connection configured for TLS-only traffic — '
359+
'Sending 400 Bad Request.',
360+
)
361+
362+
msg = (
363+
'The client sent a plain HTTP request, but this server '
364+
'only speaks HTTPS on this port.'
365+
)
366+
367+
response_parts = [
368+
f'{self.server.protocol} 400 Bad Request\r\n',
369+
'Content-Type: text/plain\r\n',
370+
f'Content-Length: {len(msg)}\r\n',
371+
'Connection: close\r\n',
372+
'\r\n',
373+
msg,
374+
]
375+
response_bytes = ''.join(response_parts).encode('ISO-8859-1')
376+
377+
try:
378+
# Handle both raw sockets and SSL connections
379+
if hasattr(sock, 'sendall'):
380+
sock.sendall(response_bytes)
381+
else:
382+
# Fallback for older PyOpenSSL or SSL objects
383+
sock.send(response_bytes)
384+
sock.shutdown(socket.SHUT_WR)
385+
except OSError as ex:
386+
if ex.args[0] not in errors.socket_errors_to_ignore:
387+
raise
388+
389+
sock.close()
390+
384391
def close(self):
385392
"""Close all monitored connections."""
386393
for _, conn in self._selector.connections:

cheroot/makefile.py

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
# prefer slower Python-based io module
44
import _pyio as io
5+
import io as stdlib_io
56
import socket
67

78

@@ -38,9 +39,16 @@ def _flush_unlocked(self):
3839
class StreamReader(io.BufferedReader):
3940
"""Socket stream reader."""
4041

41-
def __init__(self, sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
42-
"""Initialize socket stream reader."""
43-
super().__init__(socket.SocketIO(sock, mode), bufsize)
42+
def __init__(self, sock, bufsize=io.DEFAULT_BUFFER_SIZE):
43+
"""Initialize with socket or raw IO object."""
44+
# If already a RawIOBase (like TLSSocket), use directly
45+
if isinstance(sock, (io.RawIOBase, stdlib_io.RawIOBase)):
46+
raw_io = sock
47+
else:
48+
# Wrap raw socket with SocketIO
49+
raw_io = socket.SocketIO(sock, 'rb')
50+
51+
super().__init__(raw_io, bufsize)
4452
self.bytes_read = 0
4553

4654
def read(self, *args, **kwargs):
@@ -57,19 +65,20 @@ def has_data(self):
5765
class StreamWriter(BufferedWriter):
5866
"""Socket stream writer."""
5967

60-
def __init__(self, sock, mode='w', bufsize=io.DEFAULT_BUFFER_SIZE):
61-
"""Initialize socket stream writer."""
62-
super().__init__(socket.SocketIO(sock, mode), bufsize)
68+
def __init__(self, sock, bufsize=io.DEFAULT_BUFFER_SIZE):
69+
"""Initialize with socket or raw IO object."""
70+
# If already a RawIOBase (like TLSSocket), use directly
71+
if isinstance(sock, (io.RawIOBase, stdlib_io.RawIOBase)):
72+
raw_io = sock
73+
else:
74+
# Wrap raw socket with SocketIO
75+
raw_io = socket.SocketIO(sock, 'wb')
76+
77+
super().__init__(raw_io, bufsize)
6378
self.bytes_written = 0
6479

6580
def write(self, val, *args, **kwargs):
6681
"""Capture bytes written."""
6782
res = super().write(val, *args, **kwargs)
6883
self.bytes_written += len(val)
6984
return res
70-
71-
72-
def MakeFile(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
73-
"""File object attached to a socket object."""
74-
cls = StreamReader if 'r' in mode else StreamWriter
75-
return cls(sock, mode, bufsize)

cheroot/makefile.pyi

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ class BufferedWriter(io.BufferedWriter):
77

88
class StreamReader(io.BufferedReader):
99
bytes_read: int
10-
def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
10+
def __init__(self, sock, bufsize=...) -> None: ...
1111
def read(self, *args, **kwargs): ...
1212
def has_data(self): ...
1313

1414
class StreamWriter(BufferedWriter):
1515
bytes_written: int
16-
def __init__(self, sock, mode: str = ..., bufsize=...) -> None: ...
16+
def __init__(self, sock, bufsize=...) -> None: ...
1717
def write(self, val, *args, **kwargs): ...
18-
19-
def MakeFile(sock, mode: str = ..., bufsize=...): ...

cheroot/server.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@
8383

8484
from . import __version__, connections, errors
8585
from ._compat import IS_PPC, bton
86-
from .makefile import MakeFile, StreamWriter
86+
from .makefile import StreamReader, StreamWriter
8787
from .workers import threadpool
8888

8989

@@ -1275,19 +1275,18 @@ class HTTPConnection:
12751275
# Fields set by ConnectionManager.
12761276
last_used = None
12771277

1278-
def __init__(self, server, sock, makefile=MakeFile):
1278+
def __init__(self, server, sock):
12791279
"""Initialize HTTPConnection instance.
12801280
12811281
Args:
12821282
server (HTTPServer): web server object receiving this request
12831283
sock (socket._socketobject): the raw socket object (usually
12841284
TCP) for this connection
1285-
makefile (file): a fileobject class for reading from the socket
12861285
"""
12871286
self.server = server
12881287
self.socket = sock
1289-
self.rfile = makefile(sock, 'rb', self.rbufsize)
1290-
self.wfile = makefile(sock, 'wb', self.wbufsize)
1288+
self.rfile = StreamReader(sock, self.rbufsize)
1289+
self.wfile = StreamWriter(sock, self.wbufsize)
12911290
self.requests_seen = 0
12921291

12931292
self.peercreds_enabled = self.server.peercreds_enabled
@@ -1363,7 +1362,7 @@ def _handle_no_ssl(self, req):
13631362
except AttributeError:
13641363
# self.socket is of OpenSSL.SSL.Connection type
13651364
resp_sock = self.socket._socket
1366-
self.wfile = StreamWriter(resp_sock, 'wb', self.wbufsize)
1365+
self.wfile = StreamWriter(resp_sock, self.wbufsize)
13671366
msg = (
13681367
'The client sent a plain HTTP request, but '
13691368
'this server only speaks HTTPS on this port.'

cheroot/server.pyi

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ class HTTPConnection:
112112
rfile: Any
113113
wfile: Any
114114
requests_seen: int
115-
def __init__(self, server, sock, makefile=...) -> None: ...
115+
def __init__(self, server, sock) -> None: ...
116116
def communicate(self): ...
117117
linger: bool
118118
def close(self) -> None: ...

0 commit comments

Comments
 (0)