Skip to content

PYTHON-5309: [v4.12] AsyncMongoClient doesn't use PyOpenSSL (#2286) #2319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 28, 2025
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
26 changes: 16 additions & 10 deletions .evergreen/generated_configs/variants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -735,18 +735,20 @@ buildvariants:
- macos-14
batchtime: 10080
expansions:
TEST_NAME: pyopenssl
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /Library/Frameworks/Python.Framework/Versions/3.9/bin/python3
- name: pyopenssl-rhel8-python3.10
tasks:
- name: .replica_set .auth .ssl .sync
- name: .7.0 .auth .ssl .sync
- name: .replica_set .auth .ssl .sync_async
- name: .7.0 .auth .ssl .sync_async
display_name: PyOpenSSL RHEL8 Python3.10
run_on:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: pyopenssl
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.10/bin/python3
- name: pyopenssl-rhel8-python3.11
tasks:
Expand All @@ -757,7 +759,8 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: pyopenssl
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.11/bin/python3
- name: pyopenssl-rhel8-python3.12
tasks:
Expand All @@ -768,18 +771,20 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: pyopenssl
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.12/bin/python3
- name: pyopenssl-win64-python3.13
tasks:
- name: .replica_set .auth .ssl .sync
- name: .7.0 .auth .ssl .sync
- name: .replica_set .auth .ssl .sync_async
- name: .7.0 .auth .ssl .sync_async
display_name: PyOpenSSL Win64 Python3.13
run_on:
- windows-64-vsMulti-small
batchtime: 10080
expansions:
TEST_NAME: pyopenssl
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: C:/python/Python313/python.exe
- name: pyopenssl-rhel8-pypy3.10
tasks:
Expand All @@ -790,7 +795,8 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: pyopenssl
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3

# Search index tests
Expand Down
29 changes: 20 additions & 9 deletions .evergreen/scripts/generate_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ def create_enterprise_auth_variants():
def create_pyopenssl_variants():
base_name = "PyOpenSSL"
batchtime = BATCHTIME_WEEK
expansions = dict(TEST_NAME="pyopenssl")
expansions = dict(TEST_NAME="default", SUB_TEST_NAME="pyopenssl")
variants = []

for python in ALL_PYTHONS:
Expand All @@ -506,14 +506,25 @@ def create_pyopenssl_variants():
host = DEFAULT_HOST

display_name = get_variant_name(base_name, host, python=python)
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
# only need to run some on async
if python in (CPYTHONS[1], CPYTHONS[-1]):
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync_async", f".7.0 .{auth} .{ssl} .sync_async"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
else:
variant = create_variant(
[f".replica_set .{auth} .{ssl} .sync", f".7.0 .{auth} .{ssl} .sync"],
display_name,
python=python,
host=host,
expansions=expansions,
batchtime=batchtime,
)
variants.append(variant)

return variants
Expand Down
2 changes: 2 additions & 0 deletions doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Version 4.12.1 is a bug fix release.
errors such as: "NotImplementedError: Database objects do not implement truth value testing or bool()".
- Fixed a bug where MongoDB cluster topology changes could cause asynchronous operations to take much longer to complete
due to holding the Topology lock while closing stale connections.
- Fixed a bug that would cause AsyncMongoClient to attempt to use PyOpenSSL when available, resulting in errors such as
"pymongo.errors.ServerSelectionTimeoutError: 'SSLContext' object has no attribute 'wrap_bio'".

Issues Resolved
...............
Expand Down
9 changes: 7 additions & 2 deletions pymongo/asynchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
from pymongo.results import BulkWriteResult, DeleteResult
from pymongo.ssl_support import BLOCKING_IO_ERRORS, get_ssl_context
from pymongo.typings import _DocumentType, _DocumentTypeArg
from pymongo.uri_parser_shared import parse_host
from pymongo.uri_parser_shared import _parse_kms_tls_options, parse_host
from pymongo.write_concern import WriteConcern

if TYPE_CHECKING:
Expand Down Expand Up @@ -157,6 +157,7 @@ def __init__(
self.mongocryptd_client = mongocryptd_client
self.opts = opts
self._spawned = False
self._kms_ssl_contexts = opts._kms_ssl_contexts(_IS_SYNC)

async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
"""Complete a KMS request.
Expand All @@ -168,7 +169,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
endpoint = kms_context.endpoint
message = kms_context.message
provider = kms_context.kms_provider
ctx = self.opts._kms_ssl_contexts.get(provider)
ctx = self._kms_ssl_contexts.get(provider)
if ctx is None:
# Enable strict certificate verification, OCSP, match hostname, and
# SNI using the system default CA certificates.
Expand All @@ -180,6 +181,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
False, # allow_invalid_certificates
False, # allow_invalid_hostnames
False, # disable_ocsp_endpoint_check
_IS_SYNC,
)
# CSOT: set timeout for socket creation.
connect_timeout = max(_csot.clamp_remaining(_KMS_CONNECT_TIMEOUT), 0.001)
Expand Down Expand Up @@ -396,6 +398,8 @@ def __init__(self, client: AsyncMongoClient[_DocumentTypeArg], opts: AutoEncrypt
encrypted_fields_map = _dict_to_bson(opts._encrypted_fields_map, False, _DATA_KEY_OPTS)
self._bypass_auto_encryption = opts._bypass_auto_encryption
self._internal_client = None
# parsing kms_ssl_contexts here so that parsing errors will be raised before internal clients are created
opts._kms_ssl_contexts(_IS_SYNC)

def _get_internal_client(
encrypter: _Encrypter, mongo_client: AsyncMongoClient[_DocumentTypeArg]
Expand Down Expand Up @@ -675,6 +679,7 @@ def __init__(
kms_tls_options=kms_tls_options,
key_expiration_ms=key_expiration_ms,
)
self._kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
None, key_vault_coll, None, opts
)
Expand Down
6 changes: 3 additions & 3 deletions pymongo/asynchronous/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
from pymongo.network_layer import AsyncNetworkingInterface, async_receive_message, async_sendall
from pymongo.pool_options import PoolOptions
from pymongo.pool_shared import (
SSLErrors,
_CancellationContext,
_configured_protocol_interface,
_get_timeout_details,
Expand All @@ -86,7 +87,6 @@
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 SSLError

if TYPE_CHECKING:
from bson import CodecOptions
Expand Down Expand Up @@ -638,7 +638,7 @@ async def _raise_connection_failure(self, error: BaseException) -> NoReturn:
reason = ConnectionClosedReason.ERROR
await self.close_conn(reason)
# SSLError from PyOpenSSL inherits directly from Exception.
if isinstance(error, (IOError, OSError, SSLError)):
if isinstance(error, (IOError, OSError, *SSLErrors)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)
else:
Expand Down Expand Up @@ -1052,7 +1052,7 @@ async def connect(self, handler: Optional[_MongoClientErrorHandler] = None) -> A
reason=_verbose_connection_error_reason(ConnectionClosedReason.ERROR),
error=ConnectionClosedReason.ERROR,
)
if isinstance(error, (IOError, OSError, SSLError)):
if isinstance(error, (IOError, OSError, *SSLErrors)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)

Expand Down
7 changes: 5 additions & 2 deletions pymongo/client_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ def _parse_read_concern(options: Mapping[str, Any]) -> ReadConcern:
return ReadConcern(concern)


def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext], bool]:
def _parse_ssl_options(
options: Mapping[str, Any], is_sync: bool
) -> tuple[Optional[SSLContext], bool]:
"""Parse ssl options."""
use_tls = options.get("tls")
if use_tls is not None:
Expand Down Expand Up @@ -138,6 +140,7 @@ def _parse_ssl_options(options: Mapping[str, Any]) -> tuple[Optional[SSLContext]
allow_invalid_certificates,
allow_invalid_hostnames,
disable_ocsp_endpoint_check,
is_sync,
)
return ctx, allow_invalid_hostnames
return None, allow_invalid_hostnames
Expand Down Expand Up @@ -167,7 +170,7 @@ def _parse_pool_options(
compression_settings = CompressionSettings(
options.get("compressors", []), options.get("zlibcompressionlevel", -1)
)
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options)
ssl_context, tls_allow_invalid_hostnames = _parse_ssl_options(options, is_sync)
load_balanced = options.get("loadbalanced")
max_connecting = options.get("maxconnecting", common.MAX_CONNECTING)
return PoolOptions(
Expand Down
18 changes: 16 additions & 2 deletions pymongo/encryption_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

from typing import TYPE_CHECKING, Any, Mapping, Optional

from pymongo.uri_parser_shared import _parse_kms_tls_options

try:
import pymongocrypt # type:ignore[import-untyped] # noqa: F401

Expand All @@ -32,9 +34,9 @@
from bson import int64
from pymongo.common import validate_is_mapping
from pymongo.errors import ConfigurationError
from pymongo.uri_parser_shared import _parse_kms_tls_options

if TYPE_CHECKING:
from pymongo.pyopenssl_context import SSLContext
from pymongo.typings import _AgnosticMongoClient, _DocumentTypeArg


Expand Down Expand Up @@ -236,10 +238,22 @@ def __init__(
if not any("idleShutdownTimeoutSecs" in s for s in self._mongocryptd_spawn_args):
self._mongocryptd_spawn_args.append("--idleShutdownTimeoutSecs=60")
# Maps KMS provider name to a SSLContext.
self._kms_ssl_contexts = _parse_kms_tls_options(kms_tls_options)
self._kms_tls_options = kms_tls_options
self._sync_kms_ssl_contexts: Optional[dict[str, SSLContext]] = None
self._async_kms_ssl_contexts: Optional[dict[str, SSLContext]] = None
self._bypass_query_analysis = bypass_query_analysis
self._key_expiration_ms = key_expiration_ms

def _kms_ssl_contexts(self, is_sync: bool) -> dict[str, SSLContext]:
if is_sync:
if self._sync_kms_ssl_contexts is None:
self._sync_kms_ssl_contexts = _parse_kms_tls_options(self._kms_tls_options, True)
return self._sync_kms_ssl_contexts
else:
if self._async_kms_ssl_contexts is None:
self._async_kms_ssl_contexts = _parse_kms_tls_options(self._kms_tls_options, False)
return self._async_kms_ssl_contexts


class RangeOpts:
"""Options to configure encrypted queries using the range algorithm."""
Expand Down
22 changes: 9 additions & 13 deletions pymongo/network_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,22 +46,18 @@
_HAVE_SSL = False

try:
from pymongo.pyopenssl_context import (
BLOCKING_IO_LOOKUP_ERROR,
BLOCKING_IO_READ_ERROR,
BLOCKING_IO_WRITE_ERROR,
_sslConn,
)
from pymongo.pyopenssl_context import _sslConn

_HAVE_PYOPENSSL = True
except ImportError:
_HAVE_PYOPENSSL = False
_sslConn = SSLSocket # type: ignore
from pymongo.ssl_support import ( # type: ignore[assignment]
BLOCKING_IO_LOOKUP_ERROR,
BLOCKING_IO_READ_ERROR,
BLOCKING_IO_WRITE_ERROR,
)
_sslConn = SSLSocket # type: ignore[assignment, misc]

from pymongo.ssl_support import (
BLOCKING_IO_LOOKUP_ERROR,
BLOCKING_IO_READ_ERROR,
BLOCKING_IO_WRITE_ERROR,
)

if TYPE_CHECKING:
from pymongo.asynchronous.pool import AsyncConnection
Expand All @@ -71,7 +67,7 @@
_UNPACK_COMPRESSION_HEADER = struct.Struct("<iiB").unpack
_POLL_TIMEOUT = 0.5
# Errors raised by sockets (and TLS sockets) when in non-blocking mode.
BLOCKING_IO_ERRORS = (BlockingIOError, BLOCKING_IO_LOOKUP_ERROR, *ssl_support.BLOCKING_IO_ERRORS)
BLOCKING_IO_ERRORS = (BlockingIOError, *BLOCKING_IO_LOOKUP_ERROR, *ssl_support.BLOCKING_IO_ERRORS)


# These socket-based I/O methods are for KMS requests and any other network operations that do not use
Expand Down
Loading
Loading