Merge branch 'master' of github.com:mongodb/mongo-python-driver

This commit is contained in:
Steven Silvester 2025-04-27 10:59:09 -05:00
commit 0634cef571
No known key found for this signature in database
GPG Key ID: B1BF5EC3A8B32F91
23 changed files with 235 additions and 125 deletions

View File

@ -620,17 +620,19 @@ buildvariants:
- macos-14
batchtime: 10080
expansions:
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: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.10/bin/python3
- name: pyopenssl-rhel8-python3.11
@ -642,6 +644,7 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/3.11/bin/python3
- name: pyopenssl-rhel8-python3.12
@ -653,17 +656,19 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
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: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: C:/python/Python313/python.exe
- name: pyopenssl-rhel8-pypy3.10
@ -675,6 +680,7 @@ buildvariants:
- rhel87-small
batchtime: 10080
expansions:
TEST_NAME: default
SUB_TEST_NAME: pyopenssl
PYTHON_BINARY: /opt/python/pypy3.10/bin/python3

View File

@ -250,7 +250,7 @@ def create_enterprise_auth_variants():
def create_pyopenssl_variants():
base_name = "PyOpenSSL"
batchtime = BATCHTIME_WEEK
expansions = dict(SUB_TEST_NAME="pyopenssl")
expansions = dict(TEST_NAME="default", SUB_TEST_NAME="pyopenssl")
variants = []
for python in ALL_PYTHONS:
@ -265,14 +265,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

View File

@ -54,7 +54,6 @@ jobs:
queries: security-extended
config: |
paths-ignore:
- '.github/**'
- 'doc/**'
- 'tools/**'
- 'test/**'

View File

@ -16,6 +16,7 @@
.. autodata:: MD5_SUBTYPE
.. autodata:: COLUMN_SUBTYPE
.. autodata:: SENSITIVE_SUBTYPE
.. autodata:: VECTOR_SUBTYPE
.. autodata:: USER_DEFINED_SUBTYPE
.. autoclass:: UuidRepresentation

View File

@ -18,6 +18,8 @@ Version 4.12.1 is a bug fix release.
Eventlet is actively being sunset by its maintainers and has compatibility issues with PyMongo's dnspython dependency.
- 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
...............

View File

@ -87,7 +87,7 @@ from pymongo.read_concern import ReadConcern
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:
@ -157,6 +157,7 @@ class _EncryptionIO(AsyncMongoCryptCallback): # type: ignore[misc]
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.
@ -168,7 +169,7 @@ class _EncryptionIO(AsyncMongoCryptCallback): # type: ignore[misc]
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.
@ -180,6 +181,7 @@ class _EncryptionIO(AsyncMongoCryptCallback): # type: ignore[misc]
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)
@ -396,6 +398,8 @@ class _Encrypter:
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]
@ -675,6 +679,7 @@ class AsyncClientEncryption(Generic[_DocumentType]):
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
)

View File

@ -76,6 +76,7 @@ from pymongo.monitoring import (
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,
@ -86,7 +87,6 @@ from pymongo.read_preferences import ReadPreference
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
@ -638,7 +638,7 @@ class AsyncConnection:
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:
@ -1052,7 +1052,7 @@ class Pool:
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)

View File

@ -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:
@ -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
@ -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(

View File

@ -20,6 +20,8 @@ from __future__ import annotations
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
@ -32,9 +34,9 @@ except ImportError:
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
@ -236,10 +238,22 @@ class AutoEncryptionOpts:
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."""

View File

@ -46,22 +46,18 @@ except ImportError:
_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
@ -71,7 +67,7 @@ _UNPACK_HEADER = struct.Struct("<iiii").unpack
_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

View File

@ -38,8 +38,9 @@ from pymongo.errors import ( # type:ignore[attr-defined]
)
from pymongo.network_layer import AsyncNetworkingInterface, NetworkingInterface, PyMongoProtocol
from pymongo.pool_options import PoolOptions
from pymongo.ssl_support import HAS_SNI, SSLError
from pymongo.ssl_support import PYSSLError, SSLError, _has_sni
SSLErrors = (PYSSLError, SSLError)
if TYPE_CHECKING:
from pymongo.pyopenssl_context import _sslConn
from pymongo.typings import _Address
@ -138,7 +139,7 @@ def _raise_connection_failure(
msg += format_timeout_details(timeout_details)
if isinstance(error, socket.timeout):
raise NetworkTimeout(msg) from error
elif isinstance(error, SSLError) and "timed out" in str(error):
elif isinstance(error, SSLErrors) and "timed out" in str(error):
# Eventlet does not distinguish TLS network timeouts from other
# SSLErrors (https://github.com/eventlet/eventlet/issues/692).
# Luckily, we can work around this limitation because the phrase
@ -279,7 +280,7 @@ async def _async_configured_socket(
try:
# We have to pass hostname / ip address to wrap_socket
# to use SSLContext.check_hostname.
if HAS_SNI:
if _has_sni(False):
loop = asyncio.get_running_loop()
ssl_sock = await loop.run_in_executor(
None,
@ -293,7 +294,7 @@ async def _async_configured_socket(
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
@ -349,7 +350,7 @@ async def _configured_protocol_interface(
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
# mismatch, will be turned into ServerSelectionTimeoutErrors later.
@ -458,7 +459,7 @@ def _configured_socket(address: _Address, options: PoolOptions) -> Union[socket.
try:
# We have to pass hostname / ip address to wrap_socket
# to use SSLContext.check_hostname.
if HAS_SNI:
if _has_sni(True):
ssl_sock = ssl_context.wrap_socket(sock, server_hostname=host) # type: ignore[assignment, misc, unused-ignore]
else:
ssl_sock = ssl_context.wrap_socket(sock) # type: ignore[assignment, misc, unused-ignore]
@ -467,7 +468,7 @@ def _configured_socket(address: _Address, options: PoolOptions) -> Union[socket.
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
@ -507,7 +508,7 @@ def _configured_socket_interface(address: _Address, options: PoolOptions) -> Net
try:
# We have to pass hostname / ip address to wrap_socket
# to use SSLContext.check_hostname.
if HAS_SNI:
if _has_sni(True):
ssl_sock = ssl_context.wrap_socket(sock, server_hostname=host)
else:
ssl_sock = ssl_context.wrap_socket(sock)
@ -516,7 +517,7 @@ def _configured_socket_interface(address: _Address, options: PoolOptions) -> Net
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, *SSLErrors) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol

View File

@ -15,16 +15,19 @@
"""Support for SSL in PyMongo."""
from __future__ import annotations
import types
import warnings
from typing import Optional
from typing import Any, Optional, Union
from pymongo.errors import ConfigurationError
HAVE_SSL = True
HAVE_PYSSL = True
try:
import pymongo.pyopenssl_context as _ssl
import pymongo.pyopenssl_context as _pyssl
except (ImportError, AttributeError) as exc:
HAVE_PYSSL = False
if isinstance(exc, AttributeError):
warnings.warn(
"Failed to use the installed version of PyOpenSSL. "
@ -35,10 +38,10 @@ except (ImportError, AttributeError) as exc:
UserWarning,
stacklevel=2,
)
try:
import pymongo.ssl_context as _ssl # type: ignore[no-redef]
except ImportError:
HAVE_SSL = False
try:
import pymongo.ssl_context as _ssl
except ImportError:
HAVE_SSL = False
if HAVE_SSL:
@ -49,14 +52,29 @@ if HAVE_SSL:
import ssl as _stdlibssl # noqa: F401
from ssl import CERT_NONE, CERT_REQUIRED
HAS_SNI = _ssl.HAS_SNI
IPADDR_SAFE = True
if HAVE_PYSSL:
PYSSLError: Any = _pyssl.SSLError
BLOCKING_IO_ERRORS: tuple = _ssl.BLOCKING_IO_ERRORS + _pyssl.BLOCKING_IO_ERRORS
BLOCKING_IO_READ_ERROR: tuple = (_pyssl.BLOCKING_IO_READ_ERROR, _ssl.BLOCKING_IO_READ_ERROR)
BLOCKING_IO_WRITE_ERROR: tuple = (
_pyssl.BLOCKING_IO_WRITE_ERROR,
_ssl.BLOCKING_IO_WRITE_ERROR,
)
else:
PYSSLError = _ssl.SSLError
BLOCKING_IO_ERRORS = _ssl.BLOCKING_IO_ERRORS
BLOCKING_IO_READ_ERROR = (_ssl.BLOCKING_IO_READ_ERROR,)
BLOCKING_IO_WRITE_ERROR = (_ssl.BLOCKING_IO_WRITE_ERROR,)
SSLError = _ssl.SSLError
BLOCKING_IO_ERRORS = _ssl.BLOCKING_IO_ERRORS
BLOCKING_IO_READ_ERROR = _ssl.BLOCKING_IO_READ_ERROR
BLOCKING_IO_WRITE_ERROR = _ssl.BLOCKING_IO_WRITE_ERROR
BLOCKING_IO_LOOKUP_ERROR = BLOCKING_IO_READ_ERROR
def _has_sni(is_sync: bool) -> bool:
if is_sync and HAVE_PYSSL:
return _pyssl.HAS_SNI
return _ssl.HAS_SNI
def get_ssl_context(
certfile: Optional[str],
passphrase: Optional[str],
@ -65,10 +83,15 @@ if HAVE_SSL:
allow_invalid_certificates: bool,
allow_invalid_hostnames: bool,
disable_ocsp_endpoint_check: bool,
) -> _ssl.SSLContext:
is_sync: bool,
) -> Union[_pyssl.SSLContext, _ssl.SSLContext]: # type: ignore[name-defined]
"""Create and return an SSLContext object."""
if is_sync and HAVE_PYSSL:
ssl: types.ModuleType = _pyssl
else:
ssl = _ssl
verify_mode = CERT_NONE if allow_invalid_certificates else CERT_REQUIRED
ctx = _ssl.SSLContext(_ssl.PROTOCOL_SSLv23)
ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
if verify_mode != CERT_NONE:
ctx.check_hostname = not allow_invalid_hostnames
else:
@ -80,22 +103,20 @@ if HAVE_SSL:
# up to date versions of MongoDB 2.4 and above already disable
# SSLv2 and SSLv3, python disables SSLv2 by default in >= 2.7.7
# and >= 3.3.4 and SSLv3 in >= 3.4.3.
ctx.options |= _ssl.OP_NO_SSLv2
ctx.options |= _ssl.OP_NO_SSLv3
ctx.options |= _ssl.OP_NO_COMPRESSION
ctx.options |= _ssl.OP_NO_RENEGOTIATION
ctx.options |= ssl.OP_NO_SSLv2
ctx.options |= ssl.OP_NO_SSLv3
ctx.options |= ssl.OP_NO_COMPRESSION
ctx.options |= ssl.OP_NO_RENEGOTIATION
if certfile is not None:
try:
ctx.load_cert_chain(certfile, None, passphrase)
except _ssl.SSLError as exc:
except ssl.SSLError as exc:
raise ConfigurationError(f"Private key doesn't match certificate: {exc}") from None
if crlfile is not None:
if _ssl.IS_PYOPENSSL:
if ssl.IS_PYOPENSSL:
raise ConfigurationError("tlsCRLFile cannot be used with PyOpenSSL")
# Match the server's behavior.
ctx.verify_flags = getattr( # type:ignore[attr-defined]
_ssl, "VERIFY_CRL_CHECK_LEAF", 0
)
ctx.verify_flags = getattr(ssl, "VERIFY_CRL_CHECK_LEAF", 0)
ctx.load_verify_locations(crlfile)
if ca_certs is not None:
ctx.load_verify_locations(ca_certs)
@ -109,9 +130,11 @@ else:
class SSLError(Exception): # type: ignore
pass
HAS_SNI = False
IPADDR_SAFE = False
BLOCKING_IO_ERRORS = () # type:ignore[assignment]
BLOCKING_IO_ERRORS = ()
def _has_sni(is_sync: bool) -> bool: # noqa: ARG001
return False
def get_ssl_context(*dummy): # type: ignore
"""No ssl module, raise ConfigurationError."""

View File

@ -86,7 +86,7 @@ from pymongo.synchronous.cursor import Cursor
from pymongo.synchronous.database import Database
from pymongo.synchronous.mongo_client import MongoClient
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:
@ -156,6 +156,7 @@ class _EncryptionIO(MongoCryptCallback): # type: ignore[misc]
self.mongocryptd_client = mongocryptd_client
self.opts = opts
self._spawned = False
self._kms_ssl_contexts = opts._kms_ssl_contexts(_IS_SYNC)
def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
"""Complete a KMS request.
@ -167,7 +168,7 @@ class _EncryptionIO(MongoCryptCallback): # type: ignore[misc]
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.
@ -179,6 +180,7 @@ class _EncryptionIO(MongoCryptCallback): # type: ignore[misc]
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)
@ -393,6 +395,8 @@ class _Encrypter:
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: MongoClient[_DocumentTypeArg]
@ -668,6 +672,7 @@ class ClientEncryption(Generic[_DocumentType]):
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
)

View File

@ -73,6 +73,7 @@ from pymongo.monitoring import (
from pymongo.network_layer import NetworkingInterface, receive_message, sendall
from pymongo.pool_options import PoolOptions
from pymongo.pool_shared import (
SSLErrors,
_CancellationContext,
_configured_socket_interface,
_get_timeout_details,
@ -83,7 +84,6 @@ from pymongo.read_preferences import ReadPreference
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
from pymongo.synchronous.client_session import _validate_session_write_concern
from pymongo.synchronous.helpers import _handle_reauth
from pymongo.synchronous.network import command
@ -636,7 +636,7 @@ class Connection:
reason = ConnectionClosedReason.ERROR
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:
@ -1048,7 +1048,7 @@ class Pool:
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)

View File

@ -420,7 +420,10 @@ def _check_options(nodes: Sized, options: Mapping[str, Any]) -> None:
raise ConfigurationError("Cannot specify replicaSet with loadBalanced=true")
def _parse_kms_tls_options(kms_tls_options: Optional[Mapping[str, Any]]) -> dict[str, SSLContext]:
def _parse_kms_tls_options(
kms_tls_options: Optional[Mapping[str, Any]],
is_sync: bool,
) -> dict[str, SSLContext]:
"""Parse KMS TLS connection options."""
if not kms_tls_options:
return {}
@ -435,7 +438,7 @@ def _parse_kms_tls_options(kms_tls_options: Optional[Mapping[str, Any]]) -> dict
opts = _handle_security_options(opts)
opts = _normalize_options(opts)
opts = cast(_CaseInsensitiveDictionary, validate_options(opts))
ssl_context, allow_invalid_hostnames = _parse_ssl_options(opts)
ssl_context, allow_invalid_hostnames = _parse_ssl_options(opts, is_sync)
if ssl_context is None:
raise ConfigurationError("TLS is required for KMS providers")
if allow_invalid_hostnames:

View File

@ -41,6 +41,7 @@ import pytest
from pymongo.asynchronous.collection import AsyncCollection
from pymongo.asynchronous.helpers import anext
from pymongo.daemon import _spawn_daemon
from pymongo.uri_parser_shared import _parse_kms_tls_options
try:
from pymongo.pyopenssl_context import IS_PYOPENSSL
@ -141,7 +142,7 @@ class TestAutoEncryptionOpts(AsyncPyMongoTestCase):
self.assertEqual(opts._mongocryptd_bypass_spawn, False)
self.assertEqual(opts._mongocryptd_spawn_path, "mongocryptd")
self.assertEqual(opts._mongocryptd_spawn_args, ["--idleShutdownTimeoutSecs=60"])
self.assertEqual(opts._kms_ssl_contexts, {})
self.assertEqual(opts._kms_tls_options, None)
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def test_init_spawn_args(self):
@ -165,30 +166,38 @@ class TestAutoEncryptionOpts(AsyncPyMongoTestCase):
)
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def test_init_kms_tls_options(self):
async def test_init_kms_tls_options(self):
# Error cases:
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": 1})
with self.assertRaisesRegex(TypeError, r'kms_tls_options\["kmip"\] must be a dict'):
AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": 1})
AsyncMongoClient(auto_encryption_opts=opts)
tls_opts: Any
for tls_opts in [
{"kmip": {"tls": True, "tlsInsecure": True}},
{"kmip": {"tls": True, "tlsAllowInvalidCertificates": True}},
{"kmip": {"tls": True, "tlsAllowInvalidHostnames": True}},
]:
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options=tls_opts)
with self.assertRaisesRegex(ConfigurationError, "Insecure TLS options prohibited"):
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options=tls_opts)
AsyncMongoClient(auto_encryption_opts=opts)
opts = AutoEncryptionOpts(
{}, "k.d", kms_tls_options={"kmip": {"tlsCAFile": "does-not-exist"}}
)
with self.assertRaises(FileNotFoundError):
AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": {"tlsCAFile": "does-not-exist"}})
AsyncMongoClient(auto_encryption_opts=opts)
# Success cases:
tls_opts: Any
for tls_opts in [None, {}]:
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options=tls_opts)
self.assertEqual(opts._kms_ssl_contexts, {})
kms_tls_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
self.assertEqual(kms_tls_contexts, {})
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": {"tls": True}, "aws": {}})
ctx = opts._kms_ssl_contexts["kmip"]
_kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
ctx = _kms_ssl_contexts["kmip"]
self.assertEqual(ctx.check_hostname, True)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
ctx = opts._kms_ssl_contexts["aws"]
ctx = _kms_ssl_contexts["aws"]
self.assertEqual(ctx.check_hostname, True)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
opts = AutoEncryptionOpts(
@ -196,7 +205,8 @@ class TestAutoEncryptionOpts(AsyncPyMongoTestCase):
"k.d",
kms_tls_options={"kmip": {"tlsCAFile": CA_PEM, "tlsCertificateKeyFile": CLIENT_PEM}},
)
ctx = opts._kms_ssl_contexts["kmip"]
_kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
ctx = _kms_ssl_contexts["kmip"]
self.assertEqual(ctx.check_hostname, True)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
@ -2225,7 +2235,7 @@ class TestKmsTLSOptions(AsyncEncryptionIntegrationTest):
encryption = self.create_client_encryption(
providers, "keyvault.datakeys", self.client, OPTS, kms_tls_options=options
)
ctx = encryption._io_callbacks.opts._kms_ssl_contexts["aws"]
ctx = encryption._io_callbacks._kms_ssl_contexts["aws"]
if not hasattr(ctx, "check_ocsp_endpoint"):
raise self.skipTest("OCSP not enabled")
self.assertFalse(ctx.check_ocsp_endpoint)

View File

@ -43,7 +43,7 @@ from urllib.parse import quote_plus
from pymongo import AsyncMongoClient, ssl_support
from pymongo.errors import ConfigurationError, ConnectionFailure, OperationFailure
from pymongo.hello import HelloCompat
from pymongo.ssl_support import HAVE_SSL, _ssl, get_ssl_context
from pymongo.ssl_support import HAVE_PYSSL, HAVE_SSL, _ssl, get_ssl_context
from pymongo.write_concern import WriteConcern
_HAVE_PYOPENSSL = False
@ -134,7 +134,7 @@ class TestClientSSL(AsyncPyMongoTestCase):
@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
def test_use_pyopenssl_when_available(self):
self.assertTrue(_ssl.IS_PYOPENSSL)
self.assertTrue(HAVE_PYSSL)
@unittest.skipUnless(_HAVE_PYOPENSSL, "Cannot test without PyOpenSSL")
def test_load_trusted_ca_certs(self):
@ -180,7 +180,7 @@ class TestSSL(AsyncIntegrationTest):
#
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
if not hasattr(ssl, "SSLContext") and not _ssl.IS_PYOPENSSL:
if not hasattr(ssl, "SSLContext") and not HAVE_PYSSL:
self.assertRaises(
ConfigurationError,
self.simple_client,
@ -312,13 +312,13 @@ class TestSSL(AsyncIntegrationTest):
#
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
ctx = get_ssl_context(None, None, None, None, True, True, False)
ctx = get_ssl_context(None, None, None, None, True, True, False, _IS_SYNC)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(None, None, None, None, True, False, False)
ctx = get_ssl_context(None, None, None, None, True, False, False, _IS_SYNC)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(None, None, None, None, False, True, False)
ctx = get_ssl_context(None, None, None, None, False, True, False, _IS_SYNC)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(None, None, None, None, False, False, False)
ctx = get_ssl_context(None, None, None, None, False, False, False, _IS_SYNC)
self.assertTrue(ctx.check_hostname)
response = await self.client.admin.command(HelloCompat.LEGACY_CMD)
@ -379,10 +379,11 @@ class TestSSL(AsyncIntegrationTest):
)
@async_client_context.require_tlsCertificateKeyFile
@async_client_context.require_sync
@async_client_context.require_no_api_version
@ignore_deprecations
async def test_tlsCRLFile_support(self):
if not hasattr(ssl, "VERIFY_CRL_CHECK_LEAF") or _ssl.IS_PYOPENSSL:
if not hasattr(ssl, "VERIFY_CRL_CHECK_LEAF") or HAVE_PYSSL:
self.assertRaises(
ConfigurationError,
self.simple_client,
@ -473,7 +474,7 @@ class TestSSL(AsyncIntegrationTest):
)
def test_system_certs_config_error(self):
ctx = get_ssl_context(None, None, None, None, True, True, False)
ctx = get_ssl_context(None, None, None, None, True, True, False, _IS_SYNC)
if (sys.platform != "win32" and hasattr(ctx, "set_default_verify_paths")) or hasattr(
ctx, "load_default_certs"
):
@ -504,11 +505,11 @@ class TestSSL(AsyncIntegrationTest):
# Force the test on Windows, regardless of environment.
ssl_support.HAVE_WINCERTSTORE = False
try:
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False)
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, CA_PEM)
ctx = get_ssl_context(None, None, None, None, False, False, False)
ctx = get_ssl_context(None, None, None, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, ssl_support.certifi.where())
finally:
@ -525,11 +526,11 @@ class TestSSL(AsyncIntegrationTest):
if not ssl_support.HAVE_WINCERTSTORE:
raise SkipTest("Need wincertstore to test wincertstore.")
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False)
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, CA_PEM)
ctx = get_ssl_context(None, None, None, None, False, False, False)
ctx = get_ssl_context(None, None, None, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, ssl_support._WINCERTS.name)
@ -663,6 +664,16 @@ class TestSSL(AsyncIntegrationTest):
) as client:
self.assertTrue(await client.admin.command("ping"))
@async_client_context.require_async
@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
@unittest.skipUnless(HAVE_SSL, "The ssl module is not available.")
async def test_pyopenssl_ignored_in_async(self):
client = AsyncMongoClient(
"mongodb://localhost:27017?tls=true&tlsAllowInvalidCertificates=true"
)
await client.admin.command("ping") # command doesn't matter, just needs it to connect
await client.close()
if __name__ == "__main__":
unittest.main()

View File

@ -172,7 +172,7 @@ class TestStreamingProtocol(AsyncIntegrationTest):
# 2504ms: application handshake succeeds
# 2505ms: ping command succeeds
self.assertGreaterEqual(duration, 2)
self.assertLessEqual(duration, 3.5)
self.assertLessEqual(duration, 4.0)
@async_client_context.require_failCommand_appName
async def test_heartbeat_awaited_flag(self):

View File

@ -26,7 +26,7 @@ import pytest
sys.path[0:0] = [""]
import pymongo
from pymongo.ssl_support import HAS_SNI
from pymongo.ssl_support import _has_sni
pytestmark = pytest.mark.atlas_connect
@ -57,7 +57,7 @@ class TestAtlasConnect(PyMongoTestCase):
# No auth error
client.test.test.count_documents({})
@unittest.skipUnless(HAS_SNI, "Free tier requires SNI support")
@unittest.skipUnless(_has_sni(True), "Free tier requires SNI support")
def test_free_tier(self):
self.connect(URIS["ATLAS_FREE"])
@ -80,7 +80,7 @@ class TestAtlasConnect(PyMongoTestCase):
self.connect(uri)
self.assertIn("mongodb+srv://", uri)
@unittest.skipUnless(HAS_SNI, "Free tier requires SNI support")
@unittest.skipUnless(_has_sni(True), "Free tier requires SNI support")
def test_srv_free_tier(self):
self.connect_srv(URIS["ATLAS_SRV_FREE"])

View File

@ -41,6 +41,7 @@ import pytest
from pymongo.daemon import _spawn_daemon
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.helpers import next
from pymongo.uri_parser_shared import _parse_kms_tls_options
try:
from pymongo.pyopenssl_context import IS_PYOPENSSL
@ -141,7 +142,7 @@ class TestAutoEncryptionOpts(PyMongoTestCase):
self.assertEqual(opts._mongocryptd_bypass_spawn, False)
self.assertEqual(opts._mongocryptd_spawn_path, "mongocryptd")
self.assertEqual(opts._mongocryptd_spawn_args, ["--idleShutdownTimeoutSecs=60"])
self.assertEqual(opts._kms_ssl_contexts, {})
self.assertEqual(opts._kms_tls_options, None)
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def test_init_spawn_args(self):
@ -167,28 +168,36 @@ class TestAutoEncryptionOpts(PyMongoTestCase):
@unittest.skipUnless(_HAVE_PYMONGOCRYPT, "pymongocrypt is not installed")
def test_init_kms_tls_options(self):
# Error cases:
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": 1})
with self.assertRaisesRegex(TypeError, r'kms_tls_options\["kmip"\] must be a dict'):
AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": 1})
MongoClient(auto_encryption_opts=opts)
tls_opts: Any
for tls_opts in [
{"kmip": {"tls": True, "tlsInsecure": True}},
{"kmip": {"tls": True, "tlsAllowInvalidCertificates": True}},
{"kmip": {"tls": True, "tlsAllowInvalidHostnames": True}},
]:
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options=tls_opts)
with self.assertRaisesRegex(ConfigurationError, "Insecure TLS options prohibited"):
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options=tls_opts)
MongoClient(auto_encryption_opts=opts)
opts = AutoEncryptionOpts(
{}, "k.d", kms_tls_options={"kmip": {"tlsCAFile": "does-not-exist"}}
)
with self.assertRaises(FileNotFoundError):
AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": {"tlsCAFile": "does-not-exist"}})
MongoClient(auto_encryption_opts=opts)
# Success cases:
tls_opts: Any
for tls_opts in [None, {}]:
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options=tls_opts)
self.assertEqual(opts._kms_ssl_contexts, {})
kms_tls_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
self.assertEqual(kms_tls_contexts, {})
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": {"tls": True}, "aws": {}})
ctx = opts._kms_ssl_contexts["kmip"]
_kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
ctx = _kms_ssl_contexts["kmip"]
self.assertEqual(ctx.check_hostname, True)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
ctx = opts._kms_ssl_contexts["aws"]
ctx = _kms_ssl_contexts["aws"]
self.assertEqual(ctx.check_hostname, True)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
opts = AutoEncryptionOpts(
@ -196,7 +205,8 @@ class TestAutoEncryptionOpts(PyMongoTestCase):
"k.d",
kms_tls_options={"kmip": {"tlsCAFile": CA_PEM, "tlsCertificateKeyFile": CLIENT_PEM}},
)
ctx = opts._kms_ssl_contexts["kmip"]
_kms_ssl_contexts = _parse_kms_tls_options(opts._kms_tls_options, _IS_SYNC)
ctx = _kms_ssl_contexts["kmip"]
self.assertEqual(ctx.check_hostname, True)
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
@ -2217,7 +2227,7 @@ class TestKmsTLSOptions(EncryptionIntegrationTest):
encryption = self.create_client_encryption(
providers, "keyvault.datakeys", self.client, OPTS, kms_tls_options=options
)
ctx = encryption._io_callbacks.opts._kms_ssl_contexts["aws"]
ctx = encryption._io_callbacks._kms_ssl_contexts["aws"]
if not hasattr(ctx, "check_ocsp_endpoint"):
raise self.skipTest("OCSP not enabled")
self.assertFalse(ctx.check_ocsp_endpoint)

View File

@ -43,7 +43,7 @@ from urllib.parse import quote_plus
from pymongo import MongoClient, ssl_support
from pymongo.errors import ConfigurationError, ConnectionFailure, OperationFailure
from pymongo.hello import HelloCompat
from pymongo.ssl_support import HAVE_SSL, _ssl, get_ssl_context
from pymongo.ssl_support import HAVE_PYSSL, HAVE_SSL, _ssl, get_ssl_context
from pymongo.write_concern import WriteConcern
_HAVE_PYOPENSSL = False
@ -134,7 +134,7 @@ class TestClientSSL(PyMongoTestCase):
@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
def test_use_pyopenssl_when_available(self):
self.assertTrue(_ssl.IS_PYOPENSSL)
self.assertTrue(HAVE_PYSSL)
@unittest.skipUnless(_HAVE_PYOPENSSL, "Cannot test without PyOpenSSL")
def test_load_trusted_ca_certs(self):
@ -180,7 +180,7 @@ class TestSSL(IntegrationTest):
#
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
if not hasattr(ssl, "SSLContext") and not _ssl.IS_PYOPENSSL:
if not hasattr(ssl, "SSLContext") and not HAVE_PYSSL:
self.assertRaises(
ConfigurationError,
self.simple_client,
@ -312,13 +312,13 @@ class TestSSL(IntegrationTest):
#
# --sslPEMKeyFile=/path/to/pymongo/test/certificates/server.pem
# --sslCAFile=/path/to/pymongo/test/certificates/ca.pem
ctx = get_ssl_context(None, None, None, None, True, True, False)
ctx = get_ssl_context(None, None, None, None, True, True, False, _IS_SYNC)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(None, None, None, None, True, False, False)
ctx = get_ssl_context(None, None, None, None, True, False, False, _IS_SYNC)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(None, None, None, None, False, True, False)
ctx = get_ssl_context(None, None, None, None, False, True, False, _IS_SYNC)
self.assertFalse(ctx.check_hostname)
ctx = get_ssl_context(None, None, None, None, False, False, False)
ctx = get_ssl_context(None, None, None, None, False, False, False, _IS_SYNC)
self.assertTrue(ctx.check_hostname)
response = self.client.admin.command(HelloCompat.LEGACY_CMD)
@ -379,10 +379,11 @@ class TestSSL(IntegrationTest):
)
@client_context.require_tlsCertificateKeyFile
@client_context.require_sync
@client_context.require_no_api_version
@ignore_deprecations
def test_tlsCRLFile_support(self):
if not hasattr(ssl, "VERIFY_CRL_CHECK_LEAF") or _ssl.IS_PYOPENSSL:
if not hasattr(ssl, "VERIFY_CRL_CHECK_LEAF") or HAVE_PYSSL:
self.assertRaises(
ConfigurationError,
self.simple_client,
@ -473,7 +474,7 @@ class TestSSL(IntegrationTest):
)
def test_system_certs_config_error(self):
ctx = get_ssl_context(None, None, None, None, True, True, False)
ctx = get_ssl_context(None, None, None, None, True, True, False, _IS_SYNC)
if (sys.platform != "win32" and hasattr(ctx, "set_default_verify_paths")) or hasattr(
ctx, "load_default_certs"
):
@ -504,11 +505,11 @@ class TestSSL(IntegrationTest):
# Force the test on Windows, regardless of environment.
ssl_support.HAVE_WINCERTSTORE = False
try:
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False)
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, CA_PEM)
ctx = get_ssl_context(None, None, None, None, False, False, False)
ctx = get_ssl_context(None, None, None, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, ssl_support.certifi.where())
finally:
@ -525,11 +526,11 @@ class TestSSL(IntegrationTest):
if not ssl_support.HAVE_WINCERTSTORE:
raise SkipTest("Need wincertstore to test wincertstore.")
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False)
ctx = get_ssl_context(None, None, CA_PEM, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, CA_PEM)
ctx = get_ssl_context(None, None, None, None, False, False, False)
ctx = get_ssl_context(None, None, None, None, False, False, False, _IS_SYNC)
ssl_sock = ctx.wrap_socket(socket.socket())
self.assertEqual(ssl_sock.ca_certs, ssl_support._WINCERTS.name)
@ -663,6 +664,14 @@ class TestSSL(IntegrationTest):
) as client:
self.assertTrue(client.admin.command("ping"))
@client_context.require_async
@unittest.skipUnless(_HAVE_PYOPENSSL, "PyOpenSSL is not available.")
@unittest.skipUnless(HAVE_SSL, "The ssl module is not available.")
def test_pyopenssl_ignored_in_async(self):
client = MongoClient("mongodb://localhost:27017?tls=true&tlsAllowInvalidCertificates=true")
client.admin.command("ping") # command doesn't matter, just needs it to connect
client.close()
if __name__ == "__main__":
unittest.main()

View File

@ -172,7 +172,7 @@ class TestStreamingProtocol(IntegrationTest):
# 2504ms: application handshake succeeds
# 2505ms: ping command succeeds
self.assertGreaterEqual(duration, 2)
self.assertLessEqual(duration, 3.5)
self.assertLessEqual(duration, 4.0)
@client_context.require_failCommand_appName
def test_heartbeat_awaited_flag(self):

View File

@ -35,6 +35,7 @@ def check_ocsp(host: str, port: int, capath: str) -> None:
False, # allow_invalid_certificates
False, # allow_invalid_hostnames
False,
True, # is sync
) # disable_ocsp_endpoint_check
# Ensure we're using pyOpenSSL.