Merge branch 'master' of github.com:mongodb/mongo-python-driver
This commit is contained in:
commit
0634cef571
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
1
.github/workflows/codeql.yml
vendored
1
.github/workflows/codeql.yml
vendored
@ -54,7 +54,6 @@ jobs:
|
||||
queries: security-extended
|
||||
config: |
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- 'doc/**'
|
||||
- 'tools/**'
|
||||
- 'test/**'
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
.. autodata:: MD5_SUBTYPE
|
||||
.. autodata:: COLUMN_SUBTYPE
|
||||
.. autodata:: SENSITIVE_SUBTYPE
|
||||
.. autodata:: VECTOR_SUBTYPE
|
||||
.. autodata:: USER_DEFINED_SUBTYPE
|
||||
|
||||
.. autoclass:: UuidRepresentation
|
||||
|
||||
@ -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
|
||||
...............
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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."""
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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"])
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user