diff --git a/docs/source/reference-io.rst b/docs/source/reference-io.rst index 81a615e039..4d33601f17 100644 --- a/docs/source/reference-io.rst +++ b/docs/source/reference-io.rst @@ -301,7 +301,7 @@ library socket into a Trio socket: .. autofunction:: from_stdlib_socket -Unlike :func:`socket.socket`, :func:`trio.socket.socket` is a +Unlike :class:`socket.socket`, :func:`trio.socket.socket` is a function, not a class; if you want to check whether an object is a Trio socket, use ``isinstance(obj, trio.socket.SocketType)``. @@ -380,7 +380,7 @@ Socket objects additional error checking. In addition, the following methods are similar to the equivalents - in :func:`socket.socket`, but have some Trio-specific quirks: + in :class:`socket.socket`, but have some Trio-specific quirks: .. method:: connect :async: @@ -421,7 +421,7 @@ Socket objects False otherwise. The following methods are identical to their equivalents in - :func:`socket.socket`, except async, and the ones that take address + :class:`socket.socket`, except async, and the ones that take address arguments require pre-resolved addresses: * :meth:`~socket.socket.accept` @@ -437,7 +437,7 @@ Socket objects * :meth:`~socket.socket.sendmsg` (if available) All methods and attributes *not* mentioned above are identical to - their equivalents in :func:`socket.socket`: + their equivalents in :class:`socket.socket`: * :attr:`~socket.socket.family` * :attr:`~socket.socket.type` diff --git a/newsfragments/2203.bugfix.rst b/newsfragments/2203.bugfix.rst new file mode 100644 index 0000000000..55a01d737e --- /dev/null +++ b/newsfragments/2203.bugfix.rst @@ -0,0 +1,2 @@ +Add compatibility with OpenSSL 3.0 on newer Python and PyPy versions by working +around ``SSLEOFError`` not being raised properly. \ No newline at end of file diff --git a/test-requirements.in b/test-requirements.in index 9f5d6ec99f..894fc654d3 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -1,11 +1,12 @@ # For tests -pytest >= 5.0 # for faulthandler in core +pytest >= 5.0 # for faulthandler in core pytest-cov >= 2.6.0 -ipython # for the IPython traceback integration tests -pyOpenSSL # for the ssl tests -trustme # for the ssl tests -pylint # for pylint finding all symbols tests -jedi # for jedi code completion tests +ipython # for the IPython traceback integration tests +pyOpenSSL # for the ssl tests +trustme # for the ssl tests +pylint # for pylint finding all symbols tests +jedi # for jedi code completion tests +cryptography>=36.0.0 # 35.0.0 is transitive but fails # Tools black; implementation_name == "cpython" @@ -14,7 +15,8 @@ flake8 astor # code generation # https://github.com/python-trio/trio/pull/654#issuecomment-420518745 -typed_ast; implementation_name == "cpython" +# typed_ast is deprecated as of 3.8, and straight up doesn't compile on 3.10-dev as of 2021-12-13 +typed_ast; implementation_name == "cpython" and python_version < "3.8" mypy-extensions; implementation_name == "cpython" typing-extensions; implementation_name == "cpython" diff --git a/test-requirements.txt b/test-requirements.txt index dc19c3131e..f1162efdb0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,8 +1,8 @@ # -# This file is autogenerated by pip-compile +# This file is autogenerated by pip-compile with python 3.7 # To update, run: # -# pip-compile --output-file test-requirements.txt test-requirements.in +# pip-compile --output-file=test-requirements.txt test-requirements.in # astor==0.8.1 # via -r test-requirements.in @@ -25,8 +25,9 @@ click==8.0.3 # via black coverage[toml]==6.0.2 # via pytest-cov -cryptography==35.0.0 +cryptography==36.0.1 # via + # -r test-requirements.in # pyopenssl # trustme decorator==5.1.0 @@ -39,6 +40,12 @@ idna==3.3 # trustme immutables==0.16 # via -r test-requirements.in +importlib-metadata==4.2.0 + # via + # click + # flake8 + # pluggy + # pytest iniconfig==1.1.1 # via pytest ipython==7.29.0 @@ -131,14 +138,27 @@ traitlets==5.1.1 # matplotlib-inline trustme==0.9.0 # via -r test-requirements.in -typed-ast==1.4.3 ; implementation_name == "cpython" - # via -r test-requirements.in +typed-ast==1.4.3 ; implementation_name == "cpython" and python_version < "3.8" + # via + # -r test-requirements.in + # astroid + # black + # mypy typing-extensions==3.10.0.2 ; implementation_name == "cpython" # via # -r test-requirements.in + # astroid # black + # immutables + # importlib-metadata # mypy + # pylint wcwidth==0.2.5 # via prompt-toolkit wrapt==1.13.3 # via astroid +zipp==3.6.0 + # via importlib-metadata + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/trio/_highlevel_ssl_helpers.py b/trio/_highlevel_ssl_helpers.py index fb21f239e0..a339a3d238 100644 --- a/trio/_highlevel_ssl_helpers.py +++ b/trio/_highlevel_ssl_helpers.py @@ -53,6 +53,10 @@ async def open_ssl_over_tcp_stream( ) if ssl_context is None: ssl_context = ssl.create_default_context() + + if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): + ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF + return trio.SSLStream( tcp_stream, ssl_context, server_hostname=host, https_compatible=https_compatible ) diff --git a/trio/_socket.py b/trio/_socket.py index c805f829ff..4e4e603726 100644 --- a/trio/_socket.py +++ b/trio/_socket.py @@ -231,7 +231,7 @@ async def getprotobyname(name): def from_stdlib_socket(sock): - """Convert a standard library :func:`socket.socket` object into a Trio + """Convert a standard library :class:`socket.socket` object into a Trio socket object. """ @@ -271,7 +271,7 @@ def socket( proto=0, fileno=None, ): - """Create a new Trio socket, like :func:`socket.socket`. + """Create a new Trio socket, like :class:`socket.socket`. This function's behavior can be customized using :func:`set_custom_socket_factory`. diff --git a/trio/_ssl.py b/trio/_ssl.py index 5f52b6ff3b..f9bb90afd8 100644 --- a/trio/_ssl.py +++ b/trio/_ssl.py @@ -190,6 +190,16 @@ STARTING_RECEIVE_SIZE = 16384 +def _is_eof(exc): + # There appears to be a bug on Python 3.10, where SSLErrors + # aren't properly translated into SSLEOFErrors. + # This stringly-typed error check is borrowed from the AnyIO + # project. + return isinstance(exc, _stdlib_ssl.SSLEOFError) or ( + hasattr(exc, "strerror") and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror + ) + + class NeedHandshakeError(Exception): """Some :class:`SSLStream` methods can't return any meaningful data until after the handshake. If you call them before the handshake, they raise @@ -658,9 +668,9 @@ async def receive_some(self, max_bytes=None): # For some reason, EOF before handshake sometimes raises # SSLSyscallError instead of SSLEOFError (e.g. on my linux # laptop, but not on appveyor). Thanks openssl. - if self._https_compatible and isinstance( - exc.__cause__, - (_stdlib_ssl.SSLEOFError, _stdlib_ssl.SSLSyscallError), + if self._https_compatible and ( + isinstance(exc.__cause__, _stdlib_ssl.SSLSyscallError) + or _is_eof(exc.__cause__) ): await trio.lowlevel.checkpoint() return b"" @@ -683,9 +693,8 @@ async def receive_some(self, max_bytes=None): # BROKEN. But that's actually fine, because after getting an # EOF on TLS then the only thing you can do is close the # stream, and closing doesn't care about the state. - if self._https_compatible and isinstance( - exc.__cause__, _stdlib_ssl.SSLEOFError - ): + + if self._https_compatible and _is_eof(exc.__cause__): await trio.lowlevel.checkpoint() return b"" else: diff --git a/trio/tests/test_ssl.py b/trio/tests/test_ssl.py index 933e0be470..f3bff42195 100644 --- a/trio/tests/test_ssl.py +++ b/trio/tests/test_ssl.py @@ -20,7 +20,7 @@ from .._core import ClosedResourceError, BrokenResourceError from .._highlevel_open_tcp_stream import open_tcp_stream from .. import socket as tsocket -from .._ssl import SSLStream, SSLListener, NeedHandshakeError +from .._ssl import SSLStream, SSLListener, NeedHandshakeError, _is_eof from .._util import ConflictDetector from .._core.tests.tutil import slow @@ -55,6 +55,9 @@ TRIO_TEST_1_CERT = TRIO_TEST_CA.issue_server_cert("trio-test-1.example.org") SERVER_CTX = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) +if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): + SERVER_CTX.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF + TRIO_TEST_1_CERT.configure_cert(SERVER_CTX) # TLS 1.3 has a lot of changes from previous versions. So we want to run tests @@ -76,6 +79,10 @@ @pytest.fixture(scope="module", params=client_ctx_params) def client_ctx(request): ctx = ssl.create_default_context() + + if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): + ctx.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF + TRIO_TEST_CA.configure_trust(ctx) if request.param in ["default", "tls13"]: return ctx @@ -1105,7 +1112,8 @@ async def test_ssl_https_compatibility_disagreement(client_ctx): async def receive_and_expect_error(): with pytest.raises(BrokenResourceError) as excinfo: await server.receive_some(10) - assert isinstance(excinfo.value.__cause__, ssl.SSLEOFError) + + assert _is_eof(excinfo.value.__cause__) async with _core.open_nursery() as nursery: nursery.start_soon(client.aclose)