Skip to content

PYTHON-5309 Ensure AsyncMongoClient doesn't use PyOpenSSL #2286

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 62 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
efe494d
add is_sync param
sleepyStick Apr 15, 2025
b29d1ba
update contributing
sleepyStick Apr 15, 2025
d9dfb99
add vars for pyopenssl and test
sleepyStick Apr 15, 2025
c847f25
update evergreen config to run this pyopenssl on async as well (not s…
sleepyStick Apr 15, 2025
ae8ecc4
fix typing
sleepyStick Apr 15, 2025
03f4ba1
fix tests
sleepyStick Apr 16, 2025
5349164
fix test pt2
sleepyStick Apr 16, 2025
67100fc
edit evergreen config
sleepyStick Apr 16, 2025
88ae345
fix test
sleepyStick Apr 16, 2025
e451ceb
fix test errors
sleepyStick Apr 18, 2025
0312acb
fix typo...
sleepyStick Apr 18, 2025
4e85024
fix typing
sleepyStick Apr 18, 2025
dccd96a
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 18, 2025
c86a85f
maybe this works?
sleepyStick Apr 21, 2025
12ef993
fix typing
sleepyStick Apr 21, 2025
bc76aae
fix tests
sleepyStick Apr 21, 2025
a9c63c8
fix typing again
sleepyStick Apr 21, 2025
67c6738
fix tests pt 2?
sleepyStick Apr 21, 2025
3ea4de7
fix test pt3
sleepyStick Apr 21, 2025
38ad677
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 21, 2025
c57aed2
add test_name
sleepyStick Apr 21, 2025
2591169
address review
sleepyStick Apr 22, 2025
06a710d
update changelog
sleepyStick Apr 22, 2025
ef4111e
undo whitespace changes
sleepyStick Apr 22, 2025
0b3c6bb
fix failures
sleepyStick Apr 22, 2025
9336f58
fix typing
sleepyStick Apr 22, 2025
23b7cbe
fix typing?
sleepyStick Apr 22, 2025
760fa97
fix error?
sleepyStick Apr 22, 2025
4b8a4ed
undo whitespace changes
sleepyStick Apr 22, 2025
5807ba1
more whitespace changes
sleepyStick Apr 22, 2025
683ba33
move kms_ssl_contexts
sleepyStick Apr 22, 2025
d007c5f
fix import
sleepyStick Apr 22, 2025
05c061a
fix test failures
sleepyStick Apr 22, 2025
350f103
fix test failure pt2
sleepyStick Apr 22, 2025
5fa117f
change changelog line ft noah's suggestion
sleepyStick Apr 23, 2025
56c9662
_ssl -> _stdssl and ssl_in_use -> _ssl
sleepyStick Apr 23, 2025
a7324e5
make combined error type
sleepyStick Apr 23, 2025
af83d81
jk cant do _stdssl
sleepyStick Apr 23, 2025
f6b17dd
_pysslConn back to _sslConn
sleepyStick Apr 23, 2025
74ca8be
fix _ssl and has_sni
sleepyStick Apr 23, 2025
536f189
fix test
sleepyStick Apr 23, 2025
24354b4
_ssl -> ssl
sleepyStick Apr 23, 2025
4178fcc
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 23, 2025
b2324e3
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 23, 2025
17cf61d
Update pymongo/asynchronous/pool.py
sleepyStick Apr 24, 2025
257f8fe
Update doc/changelog.rst
sleepyStick Apr 24, 2025
74f98c5
Update pymongo/asynchronous/pool.py
sleepyStick Apr 24, 2025
6752a67
address review
sleepyStick Apr 24, 2025
750a9aa
fix error
sleepyStick Apr 24, 2025
e7e36b4
Merge branch 'master' into PYTHON-5309
sleepyStick Apr 24, 2025
8af8f09
fix typing
sleepyStick Apr 24, 2025
16d3cc3
Merge branch 'PYTHON-5309' of github.com:sleepyStick/mongo-python-dri…
sleepyStick Apr 24, 2025
6971fed
back to tuple type
sleepyStick Apr 24, 2025
4d12c59
remove added section
sleepyStick Apr 24, 2025
a0fe2e5
modify encryption tests
sleepyStick Apr 24, 2025
7b4ae9c
fix test
sleepyStick Apr 24, 2025
f02a791
change test
sleepyStick Apr 24, 2025
4ed055e
close the client
sleepyStick Apr 24, 2025
981a046
fix the tests pt3
sleepyStick Apr 24, 2025
c20623f
fix typing
sleepyStick Apr 24, 2025
bdaf87a
fix import
sleepyStick Apr 24, 2025
c2b2cc3
fix typing and add comment in encryption
sleepyStick Apr 24, 2025
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
8 changes: 4 additions & 4 deletions .evergreen/generated_configs/variants.yml
Original file line number Diff line number Diff line change
Expand Up @@ -740,8 +740,8 @@ buildvariants:
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
Expand Down Expand Up @@ -773,8 +773,8 @@ buildvariants:
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
Expand Down
27 changes: 19 additions & 8 deletions .evergreen/scripts/generate_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,14 +262,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
18 changes: 18 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,3 +420,21 @@ partially-converted asynchronous version of the same name to the `test/asynchron
Use this generated file as a starting point for the completed conversion.

The script is used like so: `python tools/convert_test_to_async.py [test_file.py]`

## Running PyMongo with SSL
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the motivation for adding this section?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh uh, I was struggling to get it working and asked Noah a bunch of questions, so he suggested I add a section here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned about people seeing this section and misinterpreting it to mean that they should be using tlsAllowInvalidCertificates. Can we remove it or make it very clear that this is only for local testing (using our test certificates) and not production?

Note that `AsyncMongoClient` does not support PyOpenSSL.
Assuming all required packages are installed, set the `tls` and `tlsAllowInvalidCertificates` flags in the URI to enable
the driver to connect with SSL, like so:
```python
from pymongo import MongoClient

client = MongoClient(
"mongodb://localhost:27017?tls=true&tlsAllowInvalidCertificates=true"
)
```
Another way of doing this would be to pass these options in as parameters to the MongoClient, like so:
```python
client = MongoClient(
"mongodb://localhost:27017", tls=True, tlsAllowInvalidCertificates=True
)
```
6 changes: 4 additions & 2 deletions pymongo/asynchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,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.ssl_support import BLOCKING_IO_ERRORS, PYBLOCKING_IO_ERRORS, get_ssl_context
from pymongo.typings import _DocumentType, _DocumentTypeArg
from pymongo.uri_parser_shared import parse_host
from pymongo.write_concern import WriteConcern
Expand Down Expand Up @@ -180,6 +180,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 @@ -215,7 +216,7 @@ async def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
raise # Propagate MongoCryptError errors directly.
except Exception as exc:
# Wrap I/O errors in PyMongo exceptions.
if isinstance(exc, BLOCKING_IO_ERRORS):
if isinstance(exc, (BLOCKING_IO_ERRORS, PYBLOCKING_IO_ERRORS)):
exc = socket.timeout("timed out")
# Async raises an OSError instead of returning empty bytes.
if isinstance(exc, OSError):
Expand Down Expand Up @@ -674,6 +675,7 @@ def __init__(
key_vault_namespace,
kms_tls_options=kms_tls_options,
key_expiration_ms=key_expiration_ms,
is_sync=_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 @@ -85,7 +85,7 @@
from pymongo.server_api import _add_to_command
from pymongo.server_type import SERVER_TYPE
from pymongo.socket_checker import SocketChecker
from pymongo.ssl_support import SSLError
from pymongo.ssl_support import PYSSLError, SSLError

if TYPE_CHECKING:
from bson import CodecOptions
Expand Down Expand Up @@ -637,7 +637,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, SSLError, PYSSLError)):
details = _get_timeout_details(self.opts)
_raise_connection_failure(self.address, error, timeout_details=details)
else:
Expand Down Expand Up @@ -1033,7 +1033,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, SSLError, PYSSLError)):
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
3 changes: 2 additions & 1 deletion pymongo/encryption_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ def __init__(
bypass_query_analysis: bool = False,
encrypted_fields_map: Optional[Mapping[str, Any]] = None,
key_expiration_ms: Optional[int] = None,
is_sync: bool = True,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a public api so we can't add "is_sync" here. What's the motivation for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I added is_sync as a param to a few functions because in ssl_support.get_ssl_context(), we'll need to know which version of ssl should be used,, is there another way to go about this that is preferred?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, we still can not add "is_sync" here so we need to find another way.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One way would be to lazily init the async SSLContext in kms_request().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did it? Not sure if this is what you meant, but what I have now would leave the params to AutoEncryptOpts unchanged.
I basically delayed the parse_kms_tls_options because that's where is_sync was needed. I believe i appropriately delayed the definition of kms_tls_options but let me know if I missed something. I'm not super familiar with how encryption in the driver works >.<

) -> None:
"""Options to configure automatic client-side field level encryption.

Expand Down Expand Up @@ -236,7 +237,7 @@ 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_ssl_contexts = _parse_kms_tls_options(kms_tls_options, is_sync)
self._bypass_query_analysis = bypass_query_analysis
self._key_expiration_ms = key_expiration_ms

Expand Down
12 changes: 6 additions & 6 deletions pymongo/pool_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
)
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 HAS_SNI, PYSSLError, SSLError

if TYPE_CHECKING:
from pymongo.pyopenssl_context import _sslConn
Expand Down Expand Up @@ -138,7 +138,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, (SSLError, PYSSLError)) and "timed out" in str(error):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these need to be separate SSLError and PYSSLError types, or can we export a shared one from ssl_support to reduce code churn?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, I tried to do something like SSLError = (_ssl.SSLError, _pyssl.SSLError) in ssl_support but i think it was in network_layer where we do a raise SSLError() so it needed to be a specific type. Is there another way to accomplish what you're describing that would work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The combined type could be here in pool_shared.py, as that's where it would get used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only explicitly raise SSLError in network_layer for our socket-based Windows SSL I/O. Those will go away if we do PYTHON-5215.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh i see, makes sense!

# 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
Expand Down Expand Up @@ -293,7 +293,7 @@ async def _async_configured_socket(
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, SSLError, PYSSLError) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
Expand Down Expand Up @@ -349,7 +349,7 @@ async def _configured_protocol_interface(
# Raise _CertificateError directly like we do after match_hostname
# below.
raise
except (OSError, SSLError) as exc:
except (OSError, SSLError, PYSSLError) 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.
Expand Down Expand Up @@ -467,7 +467,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, SSLError, PYSSLError) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
Expand Down Expand Up @@ -516,7 +516,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, SSLError, PYSSLError) as exc:
sock.close()
# We raise AutoReconnect for transient and permanent SSL handshake
# failures alike. Permanent handshake failures, like protocol
Expand Down
54 changes: 37 additions & 17 deletions pymongo/ssl_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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. "
Expand All @@ -35,10 +38,10 @@
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:
Expand All @@ -57,6 +60,20 @@
BLOCKING_IO_WRITE_ERROR = _ssl.BLOCKING_IO_WRITE_ERROR
BLOCKING_IO_LOOKUP_ERROR = BLOCKING_IO_READ_ERROR

if HAVE_PYSSL:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we use Union types here to remove the need for two separate exports of everything? Something like:

BLOCKING_IO_ERRORS = _ssl.BLOCKING_IO_ERRORS | _pyssl.BLOCKING_IO_ERRORS

Are there any situations where we specifically care if an exception type is from PyOpenSSL or stdlib SSL?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ooo good call out. I don't think we particularly care if its pyopenssl vs stdlib ssl error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think python3.9 didn't love the union types? so i just made them tuples. But its a similar idea.
I couldn't apply this to SSLError though because sometimes we'd raise SSLError so it needed to be one specific type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand on why the union types didn't work? We use Union elsewhere in our type hints.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm okay not sure what i did incorrectly last time, but it works now! sorry about that >.<
(I'm going to guess i handled BLOCKING_IO_ERRORS incorrectly cuz i believe that's already a tuple of types and the union of two different types of tuples was not good?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait,, how am i supposed to do it then? I thought Union[x, y] was for type hints??

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That error is from trying to use the pipe | operator, right? What happens when you use Union[x, y] instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, it works! thanks!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, this is a runtime object, not a type hint. We should be using the same type as ssl.BLOCKING_IO_ERRORS (most likely a tuple or a list)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, it didn't immediately error (as a runtime object) so i thought it was just something I didn't know about how python works HAHA but okay, changing it back to a tuple now xD

PYSSLError: Any = _pyssl.SSLError
PYBLOCKING_IO_ERRORS: Any = _pyssl.BLOCKING_IO_ERRORS
PYBLOCKING_IO_READ_ERROR: Any = _pyssl.BLOCKING_IO_READ_ERROR
PYBLOCKING_IO_WRITE_ERROR: Any = _pyssl.BLOCKING_IO_WRITE_ERROR
PYBLOCKING_IO_LOOKUP_ERROR: Any = BLOCKING_IO_READ_ERROR
else:
# just make them the same as SSL so imports won't error
PYSSLError = _ssl.SSLError
PYBLOCKING_IO_ERRORS = _ssl.BLOCKING_IO_ERRORS
PYBLOCKING_IO_READ_ERROR = _ssl.BLOCKING_IO_READ_ERROR
PYBLOCKING_IO_WRITE_ERROR = _ssl.BLOCKING_IO_WRITE_ERROR
PYBLOCKING_IO_LOOKUP_ERROR = BLOCKING_IO_READ_ERROR

def get_ssl_context(
certfile: Optional[str],
passphrase: Optional[str],
Expand All @@ -65,10 +82,15 @@ def get_ssl_context(
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_in_use: types.ModuleType = _pyssl
else:
ssl_in_use = _ssl
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we rename ssl_in_use -> _ssl that way there's less code churn.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so? because _ssl is referring to stdlib ssl and i don't think we'd want it to be replaced by pyopenssl just because pyopenssl is installed?

Copy link
Member

@ShaneHarvey ShaneHarvey Apr 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a local variable assignment so what's the issue?

        if is_sync and HAVE_PYSSL:
            _ssl: types.ModuleType = _pyssl
        else:
            _ssl = _ssl

Or even just:

        if is_sync and HAVE_PYSSL:
            _ssl: types.ModuleType = _pyssl

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, no you're right -- I've been conditioned to think that shadowing a global var with a local var is generally bad for code readability(?) so my brain forgot that it was something we could do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er, am i doing it incorrectly? I'm seeing the error "UnboundLocalError: local variable '_ssl' referenced before assignment"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, I swore I've done this before. I guess not. The only way around that is like this:

        if is_sync and HAVE_PYSSL:
            _ssl: types.ModuleType = _pyssl
        else:
            _ssl = globals()["_ssl"]

Or by renaming the global "_ssl".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually kinda wanted to rename the global _ssl except i think there were some other modules that imported _ssl from ssl_support and then i wasn't sure if i was allowed to change it...

Copy link
Member

@ShaneHarvey ShaneHarvey Apr 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You know what, I think it's simpler to go back to the the local ssl_in_use var or just rename ssl_in_use to ssl? Apologies for the back and forth.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good, went with ssl because its shorter.

verify_mode = CERT_NONE if allow_invalid_certificates else CERT_REQUIRED
ctx = _ssl.SSLContext(_ssl.PROTOCOL_SSLv23)
ctx = ssl_in_use.SSLContext(ssl_in_use.PROTOCOL_SSLv23)
if verify_mode != CERT_NONE:
ctx.check_hostname = not allow_invalid_hostnames
else:
Expand All @@ -80,22 +102,20 @@ def get_ssl_context(
# 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_in_use.OP_NO_SSLv2
ctx.options |= ssl_in_use.OP_NO_SSLv3
ctx.options |= ssl_in_use.OP_NO_COMPRESSION
ctx.options |= ssl_in_use.OP_NO_RENEGOTIATION
if certfile is not None:
try:
ctx.load_cert_chain(certfile, None, passphrase)
except _ssl.SSLError as exc:
except ssl_in_use.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_in_use.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_in_use, "VERIFY_CRL_CHECK_LEAF", 0)
ctx.load_verify_locations(crlfile)
if ca_certs is not None:
ctx.load_verify_locations(ca_certs)
Expand Down
6 changes: 4 additions & 2 deletions pymongo/synchronous/encryption.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,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.ssl_support import BLOCKING_IO_ERRORS, PYBLOCKING_IO_ERRORS, get_ssl_context
from pymongo.synchronous.collection import Collection
from pymongo.synchronous.cursor import Cursor
from pymongo.synchronous.database import Database
Expand Down Expand Up @@ -179,6 +179,7 @@ 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 @@ -214,7 +215,7 @@ def kms_request(self, kms_context: MongoCryptKmsContext) -> None:
raise # Propagate MongoCryptError errors directly.
except Exception as exc:
# Wrap I/O errors in PyMongo exceptions.
if isinstance(exc, BLOCKING_IO_ERRORS):
if isinstance(exc, (BLOCKING_IO_ERRORS, PYBLOCKING_IO_ERRORS)):
exc = socket.timeout("timed out")
# Async raises an OSError instead of returning empty bytes.
if isinstance(exc, OSError):
Expand Down Expand Up @@ -667,6 +668,7 @@ def __init__(
key_vault_namespace,
kms_tls_options=kms_tls_options,
key_expiration_ms=key_expiration_ms,
is_sync=_IS_SYNC,
)
self._io_callbacks: Optional[_EncryptionIO] = _EncryptionIO(
None, key_vault_coll, None, opts
Expand Down
Loading
Loading