Skip to content

Commit 9a2d246

Browse files
committed
Deprecated Makefile in favor of StreamReader and StreamWriter
We replace the `MakeFile` factory function with direct class instantiation. The `MakeFile` function now emits a `DeprecationWarning` and will be removed in a future release. `makefile()` methods are also deprecated on the adapters and its base class.
1 parent c5d5175 commit 9a2d246

File tree

13 files changed

+458
-97
lines changed

13 files changed

+458
-97
lines changed

.flake8

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ per-file-ignores =
134134
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
135135
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
136136
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
137-
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
137+
cheroot/ssl/pyopenssl.py: C815, DAR101, DAR201, DAR401, I001, I003, I005, N801, N804, RST304, WPS100, WPS110, WPS111, WPS117, WPS120, WPS121, WPS130, WPS202, WPS210, WPS220, WPS221, WPS225, WPS229, WPS231, WPS238, WPS301, WPS335, WPS338, WPS420, WPS422, WPS430, WPS432, WPS501, WPS504, WPS505, WPS601, WPS608, WPS615
138138
cheroot/test/conftest.py: DAR101, DAR201, DAR301, I001, I003, I005, WPS100, WPS130, WPS325, WPS354, WPS420, WPS422, WPS430, WPS457
139139
cheroot/test/helper.py: DAR101, DAR201, DAR401, I001, I003, I004, N802, WPS110, WPS111, WPS121, WPS201, WPS220, WPS231, WPS301, WPS414, WPS421, WPS422, WPS505
140140
cheroot/test/test_cli.py: DAR101, DAR201, I001, I005, N802, S101, S108, WPS110, WPS421, WPS431, WPS473

cheroot/connections.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from . import errors
1313
from ._compat import IS_WINDOWS
14-
from .makefile import MakeFile
1514

1615

1716
try:
@@ -304,7 +303,6 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
304303
if hasattr(s, 'settimeout'):
305304
s.settimeout(self.server.timeout)
306305

307-
mf = MakeFile
308306
ssl_env = {}
309307
# if ssl cert and key are set, we try to be a secure HTTP server
310308
if self.server.ssl_adapter is not None:
@@ -327,12 +325,12 @@ def _from_server_socket(self, server_socket): # noqa: C901 # FIXME
327325
)
328326
self._send_bad_request_plain_http_error(s)
329327
return None
330-
mf = self.server.ssl_adapter.makefile
328+
331329
# Re-apply our timeout since we may have a new socket object
332330
if hasattr(s, 'settimeout'):
333331
s.settimeout(self.server.timeout)
334332

335-
conn = self.server.ConnectionClass(self.server, s, mf)
333+
conn = self.server.ConnectionClass(self.server, s)
336334

337335
if not isinstance(self.server.bind_addr, (str, bytes)):
338336
# optional values

cheroot/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class NoSSLError(Exception):
1515
"""Exception raised when a client speaks HTTP to an HTTPS socket."""
1616

1717

18-
class FatalSSLAlert(Exception):
18+
class FatalSSLAlert(ConnectionError):
1919
"""Exception raised when the SSL implementation signals a fatal alert."""
2020

2121

cheroot/makefile.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# prefer slower Python-based io module
44
import _pyio as io
55
import socket
6+
from warnings import warn as _warn
67

78

89
# Write only 16K at a time to sockets
@@ -70,6 +71,16 @@ def write(self, val, *args, **kwargs):
7071

7172

7273
def MakeFile(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
73-
"""File object attached to a socket object."""
74+
"""
75+
File object attached to a socket object.
76+
77+
This function is now deprecated: Use
78+
StreamReader or StreamWriter directly.
79+
"""
80+
_warn(
81+
'MakeFile is deprecated. Use StreamReader or StreamWriter directly.',
82+
DeprecationWarning,
83+
stacklevel=2,
84+
)
7485
cls = StreamReader if 'r' in mode else StreamWriter
7586
return cls(sock, mode, bufsize)

cheroot/server.py

Lines changed: 38 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,16 @@
8484

8585
from . import __version__, connections, errors
8686
from ._compat import IS_PPC, bton
87-
from .makefile import MakeFile, StreamWriter
87+
from .makefile import StreamReader, StreamWriter
8888
from .workers import threadpool
8989

9090

91+
try:
92+
from OpenSSL import SSL
93+
except ImportError:
94+
SSL = None # OpenSSL not available
95+
96+
9197
__all__ = (
9298
'ChunkedRFile',
9399
'DropUnderscoreHeaderReader',
@@ -1276,19 +1282,31 @@ class HTTPConnection:
12761282
# Fields set by ConnectionManager.
12771283
last_used = None
12781284

1279-
def __init__(self, server, sock, makefile=MakeFile):
1285+
def __init__(self, server, sock, makefile=None):
12801286
"""Initialize HTTPConnection instance.
12811287
12821288
Args:
12831289
server (HTTPServer): web server object receiving this request
12841290
sock (socket._socketobject): the raw socket object (usually
12851291
TCP) for this connection
1286-
makefile (file): a fileobject class for reading from the socket
1292+
makefile (file): Now deprecated.
1293+
Used to be a fileobject class for reading from the socket.
12871294
"""
12881295
self.server = server
12891296
self.socket = sock
1290-
self.rfile = makefile(sock, 'rb', self.rbufsize)
1291-
self.wfile = makefile(sock, 'wb', self.wbufsize)
1297+
1298+
if makefile is not None:
1299+
_warn(
1300+
'The `makefile` parameter in creating an `HTTPConnection` '
1301+
'is deprecated and will be removed in a future version. '
1302+
'The connection socket should now be fully wrapped by the '
1303+
'adapter before being passed to this constructor.',
1304+
DeprecationWarning,
1305+
stacklevel=2,
1306+
)
1307+
1308+
self.rfile = StreamReader(sock, 'rb', self.rbufsize)
1309+
self.wfile = StreamWriter(sock, 'wb', self.wbufsize)
12921310
self.requests_seen = 0
12931311

12941312
self.peercreds_enabled = self.server.peercreds_enabled
@@ -1320,6 +1338,8 @@ def communicate(self): # noqa: C901 # FIXME
13201338
req.respond()
13211339
if not req.close_connection:
13221340
return True
1341+
except errors.FatalSSLAlert:
1342+
pass
13231343
except socket.error as ex:
13241344
errnum = ex.args[0]
13251345
# sadly SSL sockets return a different (longer) time out string
@@ -1340,8 +1360,6 @@ def communicate(self): # noqa: C901 # FIXME
13401360
self._conditional_error(req, '500 Internal Server Error')
13411361
except (KeyboardInterrupt, SystemExit):
13421362
raise
1343-
except errors.FatalSSLAlert:
1344-
pass
13451363
except errors.NoSSLError:
13461364
self._handle_no_ssl(req)
13471365
except Exception as ex:
@@ -1358,12 +1376,8 @@ def communicate(self): # noqa: C901 # FIXME
13581376
def _handle_no_ssl(self, req):
13591377
if not req or req.sent_headers:
13601378
return
1361-
# Unwrap wfile
1362-
try:
1363-
resp_sock = self.socket._sock
1364-
except AttributeError:
1365-
# self.socket is of OpenSSL.SSL.Connection type
1366-
resp_sock = self.socket._socket
1379+
# Unwrap to get raw TCP socket
1380+
resp_sock = self.socket._sock
13671381
self.wfile = StreamWriter(resp_sock, 'wb', self.wbufsize)
13681382
msg = (
13691383
'The client sent a plain HTTP request, but '
@@ -1507,20 +1521,23 @@ def peer_group(self):
15071521

15081522
def _close_kernel_socket(self):
15091523
"""Terminate the connection at the transport level."""
1510-
# Honor ``sock_shutdown`` for PyOpenSSL connections.
1511-
shutdown = getattr(
1512-
self.socket,
1513-
'sock_shutdown',
1514-
self.socket.shutdown,
1515-
)
1516-
15171524
try:
1518-
shutdown(socket.SHUT_RDWR) # actually send a TCP FIN
1525+
self.socket.shutdown(socket.SHUT_RDWR)
15191526
except errors.acceptable_sock_shutdown_exceptions:
15201527
pass
15211528
except socket.error as e:
15221529
if e.errno not in errors.acceptable_sock_shutdown_error_codes:
15231530
raise
1531+
except Exception as exc:
1532+
# Normalize SSL exceptions if SSL is present
1533+
if SSL is not None and isinstance(
1534+
exc,
1535+
(SSL.Error, SSL.SysCallError),
1536+
):
1537+
raise errors.FatalSSLAlert(
1538+
'TLS/SSL connection failure',
1539+
) from exc
1540+
raise # re-raise everything else
15241541

15251542

15261543
class HTTPServer:

cheroot/ssl/__init__.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,27 @@ def _ensure_peer_speaks_https(raw_socket, /) -> None:
5656

5757

5858
class Adapter(ABC):
59-
"""Base class for SSL driver library adapters.
59+
"""
60+
Base class for SSL driver library adapters.
6061
6162
Required methods:
6263
6364
* ``wrap(sock) -> (wrapped socket, ssl environ dict)``
64-
* ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
65-
socket file object``
65+
* ``get_environ() -> (ssl environ dict)``
66+
67+
Optional methods:
68+
69+
* ``makefile(sock, mode='r', bufsize=-1) -> socket file object``
70+
71+
This method is deprecated and will be removed in a future release.
72+
73+
Historically, the ``PyOpenSSL`` adapter used ``makefile()`` to
74+
wrap the underlying socket in an ``OpenSSL``-aware file object
75+
so that Cheroot's HTTP request parser (which expects file-like I/O
76+
such as ``readline()``) could read from TLS connections. The
77+
adapter now fully wraps the socket in a TLSSocket object that
78+
implements the necessary socket and file-like
79+
methods directly, so ``makefile()`` is no longer needed.
6680
"""
6781

6882
@abstractmethod
@@ -110,9 +124,14 @@ def get_environ(self):
110124
"""Return WSGI environ entries to be merged into each request."""
111125
raise NotImplementedError # pragma: no cover
112126

113-
@abstractmethod
114127
def makefile(self, sock, mode='r', bufsize=-1):
115-
"""Return socket file object."""
128+
"""
129+
Return socket file object.
130+
131+
This method is now deprecated. It will be removed in a future version.
132+
133+
:raises NotImplementedError: Must be overridden by subclasses.
134+
"""
116135
raise NotImplementedError # pragma: no cover
117136

118137
def _prompt_for_tls_password(self) -> str:

cheroot/ssl/builtin.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,39 @@
1111
import sys
1212
import threading
1313
from contextlib import suppress
14+
from warnings import warn as _warn
1415

1516

1617
try:
1718
import ssl
1819
except ImportError:
1920
ssl = None
2021

21-
try:
22-
from _pyio import DEFAULT_BUFFER_SIZE
23-
except ImportError:
24-
try:
25-
from io import DEFAULT_BUFFER_SIZE
26-
except ImportError:
27-
DEFAULT_BUFFER_SIZE = -1
28-
2922
from .. import errors
30-
from ..makefile import StreamReader, StreamWriter
3123
from ..server import HTTPServer
3224
from . import Adapter
3325

3426

27+
# WPS413 is to suppress linter error:
28+
# bad magic module function: __getattr__
29+
# DEFAULT_BUFFER_SIZE is deprecated so this module method will be removed
30+
# in a future release
31+
def __getattr__(name): # noqa: WPS413
32+
if name == 'DEFAULT_BUFFER_SIZE':
33+
_warn(
34+
(
35+
'`DEFAULT_BUFFER_SIZE` is deprecated and '
36+
'will be removed in a future release.'
37+
),
38+
DeprecationWarning,
39+
stacklevel=2,
40+
)
41+
from io import DEFAULT_BUFFER_SIZE
42+
43+
return DEFAULT_BUFFER_SIZE
44+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
45+
46+
3547
def _assert_ssl_exc_contains(exc, *msgs):
3648
"""Check whether SSL exception contains either of messages provided."""
3749
if len(msgs) < 1:
@@ -496,7 +508,19 @@ def _make_env_dn_dict(self, env_prefix, cert_value):
496508
env['%s_%s_%i' % (env_prefix, attr_code, i)] = val
497509
return env
498510

499-
def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
500-
"""Return socket file object."""
501-
cls = StreamReader if 'r' in mode else StreamWriter
502-
return cls(sock, mode, bufsize)
511+
def makefile(self, sock, mode='r', bufsize=-1):
512+
"""
513+
Return socket file object.
514+
515+
``makefile`` is now deprecated and will be removed in a future
516+
version.
517+
"""
518+
_warn(
519+
'The `makefile` method is deprecated and will be removed in a future version. '
520+
'The connection socket should be fully wrapped by the adapter '
521+
'before being passed to the HTTPConnection constructor.',
522+
DeprecationWarning,
523+
stacklevel=2,
524+
)
525+
526+
return sock.makefile(mode, bufsize)

cheroot/ssl/builtin.pyi

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ import typing as _t
33

44
from . import Adapter
55

6-
DEFAULT_BUFFER_SIZE: int
7-
86
class BuiltinSSLAdapter(Adapter):
97
CERT_KEY_TO_ENV: _t.Any
108
CERT_KEY_TO_LDAP_CODE: _t.Any

0 commit comments

Comments
 (0)