Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pymongo/asynchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import logging
import os
import socket
import ssl
import sys
import time
import weakref
Expand Down Expand Up @@ -88,6 +87,7 @@
from pymongo.server_api import _add_to_command
from pymongo.server_type import SERVER_TYPE
from pymongo.socket_checker import SocketChecker
from pymongo.ssl_support import SSL_EOF_ERRORS

if TYPE_CHECKING:
from bson import CodecOptions
Expand Down Expand Up @@ -976,7 +976,7 @@ def _handle_connection_error(self, error: BaseException) -> None:
if isinstance(error.__cause__, (_CertificateError, SSLErrors, socket.gaierror)):
# End of file errors are excluded, because the server may have disconnected
# during the handshake.
if not isinstance(error.__cause__, (ssl.SSLEOFError, ssl.SSLZeroReturnError)):
if not isinstance(error.__cause__, SSL_EOF_ERRORS):
return
error._add_error_label("SystemOverloadedError")
error._add_error_label("RetryableError")
Expand Down
4 changes: 4 additions & 0 deletions pymongo/pyopenssl_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ def _ragged_eof(exc: BaseException) -> bool:
return exc.args == (-1, "Unexpected EOF")


# PyOpenSSL's equivalent of stdlib's ssl.SSLEOFError/ssl.SSLZeroReturnError.
EOF_ERRORS = (_SSL.SysCallError, _SSL.ZeroReturnError)


# https://github.com/pyca/pyopenssl/issues/168
# https://github.com/pyca/pyopenssl/issues/176
# https://docs.python.org/3/library/ssl.html#notes-on-non-blocking-sockets
Expand Down
9 changes: 8 additions & 1 deletion pymongo/ssl_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
# CPython ssl module constants to configure certificate verification
# at a high level. This is legacy behavior, but requires us to
# import the ssl module even if we're only using it for this purpose.
import ssl as _stdlibssl # noqa: F401
import ssl as _stdlibssl
from ssl import CERT_NONE, CERT_REQUIRED

IPADDR_SAFE = True
Expand All @@ -68,11 +68,17 @@
_pyssl.BLOCKING_IO_WRITE_ERROR,
_ssl.BLOCKING_IO_WRITE_ERROR,
)
SSL_EOF_ERRORS: tuple = ( # type: ignore[type-arg]
_stdlibssl.SSLEOFError,
_stdlibssl.SSLZeroReturnError,
*_pyssl.EOF_ERRORS,
)
Comment thread
blink1073 marked this conversation as resolved.
else:
PYSSLError = _ssl.SSLError
BLOCKING_IO_ERRORS: tuple = _ssl.BLOCKING_IO_ERRORS # type: ignore[type-arg, no-redef]
BLOCKING_IO_READ_ERROR: tuple = (_ssl.BLOCKING_IO_READ_ERROR,) # type: ignore[type-arg, no-redef]
BLOCKING_IO_WRITE_ERROR: tuple = (_ssl.BLOCKING_IO_WRITE_ERROR,) # type: ignore[type-arg, no-redef]
SSL_EOF_ERRORS: tuple = (_stdlibssl.SSLEOFError, _stdlibssl.SSLZeroReturnError) # type: ignore[type-arg, no-redef]
SSLError = _ssl.SSLError
BLOCKING_IO_LOOKUP_ERROR = BLOCKING_IO_READ_ERROR

Expand Down Expand Up @@ -138,6 +144,7 @@ class SSLError(Exception): # type: ignore

IPADDR_SAFE = False
BLOCKING_IO_ERRORS: tuple = () # type: ignore[type-arg, no-redef]
SSL_EOF_ERRORS: tuple = () # type: ignore[type-arg, no-redef]

def _has_sni(is_sync: bool) -> bool: # noqa: ARG001
return False
Expand Down
4 changes: 2 additions & 2 deletions pymongo/synchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import logging
import os
import socket
import ssl
import sys
import time
import weakref
Expand Down Expand Up @@ -85,6 +84,7 @@
from pymongo.server_api import _add_to_command
from pymongo.server_type import SERVER_TYPE
from pymongo.socket_checker import SocketChecker
from pymongo.ssl_support import SSL_EOF_ERRORS
from pymongo.synchronous.client_session import _validate_session_write_concern
from pymongo.synchronous.command_runner import run_command
from pymongo.synchronous.helpers import _handle_reauth
Expand Down Expand Up @@ -972,7 +972,7 @@ def _handle_connection_error(self, error: BaseException) -> None:
if isinstance(error.__cause__, (_CertificateError, SSLErrors, socket.gaierror)):
# End of file errors are excluded, because the server may have disconnected
# during the handshake.
if not isinstance(error.__cause__, (ssl.SSLEOFError, ssl.SSLZeroReturnError)):
if not isinstance(error.__cause__, SSL_EOF_ERRORS):
return
error._add_error_label("SystemOverloadedError")
error._add_error_label("RetryableError")
Expand Down
54 changes: 54 additions & 0 deletions test/asynchronous/test_pooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import platform
import random
import socket
import ssl
import sys
import time

Expand All @@ -41,6 +42,13 @@
from test.asynchronous.helpers import ConcurrentRunner
from test.utils_shared import delay

try:
import OpenSSL

_HAVE_PYOPENSSL = True
except ImportError:
_HAVE_PYOPENSSL = False
Comment thread
blink1073 marked this conversation as resolved.

_IS_SYNC = False


Expand Down Expand Up @@ -651,5 +659,51 @@ async def test_max_pool_size_with_connection_failure(self):
self.assertNotIn("waiting for socket from pool", str(context.exception))


class TestPoolHandleConnectionError(unittest.TestCase):
"""PYTHON-5919: PyOpenSSL raises OpenSSL.SSL.SysCallError/ZeroReturnError
(not ssl.SSLEOFError/ssl.SSLZeroReturnError) when the server closes the
socket during the TLS handshake, e.g. when an ingress rate limiter rejects
a connection. Pool._handle_connection_error must recognize these as
handshake-EOF errors and still add the SystemOverloadedError label.
"""

def _make_pool(self):
return Pool(("localhost", 27017), PoolOptions())

def test_stdlib_ssl_eof_error_is_labeled_overloaded(self):
pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = ssl.SSLEOFError("EOF occurred in violation of protocol")
pool._handle_connection_error(err)
self.assertTrue(err.has_error_label("SystemOverloadedError"))

@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
def test_pyopenssl_syscall_error_is_labeled_overloaded(self):
from OpenSSL.SSL import SysCallError

pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = SysCallError(-1, "Unexpected EOF")
pool._handle_connection_error(err)
self.assertTrue(err.has_error_label("SystemOverloadedError"))

@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
def test_pyopenssl_zero_return_error_is_labeled_overloaded(self):
from OpenSSL.SSL import ZeroReturnError

pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = ZeroReturnError()
pool._handle_connection_error(err)
self.assertTrue(err.has_error_label("SystemOverloadedError"))

def test_certificate_error_is_not_labeled_overloaded(self):
pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = ssl.SSLCertVerificationError("certificate verify failed")
pool._handle_connection_error(err)
self.assertFalse(err.has_error_label("SystemOverloadedError"))


if __name__ == "__main__":
unittest.main()
54 changes: 54 additions & 0 deletions test/test_pooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import platform
import random
import socket
import ssl
import sys
import time

Expand All @@ -41,6 +42,13 @@
from test.helpers import ConcurrentRunner
from test.utils_shared import delay

try:
import OpenSSL

_HAVE_PYOPENSSL = True
except ImportError:
_HAVE_PYOPENSSL = False

_IS_SYNC = True


Expand Down Expand Up @@ -649,5 +657,51 @@ def test_max_pool_size_with_connection_failure(self):
self.assertNotIn("waiting for socket from pool", str(context.exception))


class TestPoolHandleConnectionError(unittest.TestCase):
"""PYTHON-5919: PyOpenSSL raises OpenSSL.SSL.SysCallError/ZeroReturnError
(not ssl.SSLEOFError/ssl.SSLZeroReturnError) when the server closes the
socket during the TLS handshake, e.g. when an ingress rate limiter rejects
a connection. Pool._handle_connection_error must recognize these as
handshake-EOF errors and still add the SystemOverloadedError label.
"""

def _make_pool(self):
return Pool(("localhost", 27017), PoolOptions())

def test_stdlib_ssl_eof_error_is_labeled_overloaded(self):
pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = ssl.SSLEOFError("EOF occurred in violation of protocol")
pool._handle_connection_error(err)
self.assertTrue(err.has_error_label("SystemOverloadedError"))

@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
def test_pyopenssl_syscall_error_is_labeled_overloaded(self):
from OpenSSL.SSL import SysCallError

pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = SysCallError(-1, "Unexpected EOF")
pool._handle_connection_error(err)
self.assertTrue(err.has_error_label("SystemOverloadedError"))

@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
def test_pyopenssl_zero_return_error_is_labeled_overloaded(self):
from OpenSSL.SSL import ZeroReturnError

pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = ZeroReturnError()
pool._handle_connection_error(err)
self.assertTrue(err.has_error_label("SystemOverloadedError"))

def test_certificate_error_is_not_labeled_overloaded(self):
pool = self._make_pool()
err = AutoReconnect("connection closed")
err.__cause__ = ssl.SSLCertVerificationError("certificate verify failed")
pool._handle_connection_error(err)
self.assertFalse(err.has_error_label("SystemOverloadedError"))


if __name__ == "__main__":
unittest.main()
Loading