Skip to content

Commit 78476d0

Browse files
authored
PYTHON-3187 Avoid tight poll() loop on pyopenssl connections (#953)
1 parent 9f191d6 commit 78476d0

File tree

5 files changed

+29
-50
lines changed

5 files changed

+29
-50
lines changed

pymongo/pool.py

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import collections
1616
import contextlib
1717
import copy
18-
import ipaddress
1918
import os
2019
import platform
2120
import socket
@@ -61,20 +60,7 @@
6160
from pymongo.server_api import _add_to_command
6261
from pymongo.server_type import SERVER_TYPE
6362
from pymongo.socket_checker import SocketChecker
64-
from pymongo.ssl_support import HAS_SNI as _HAVE_SNI
65-
from pymongo.ssl_support import IPADDR_SAFE as _IPADDR_SAFE
66-
from pymongo.ssl_support import SSLError as _SSLError
67-
68-
69-
# For SNI support. According to RFC6066, section 3, IPv4 and IPv6 literals are
70-
# not permitted for SNI hostname.
71-
def is_ip_address(address):
72-
try:
73-
ipaddress.ip_address(address)
74-
return True
75-
except (ValueError, UnicodeError): # noqa: B014
76-
return False
77-
63+
from pymongo.ssl_support import HAS_SNI, SSLError
7864

7965
try:
8066
from fcntl import F_GETFD, F_SETFD, FD_CLOEXEC, fcntl
@@ -263,7 +249,7 @@ def _raise_connection_failure(
263249
msg = msg_prefix + msg
264250
if isinstance(error, socket.timeout):
265251
raise NetworkTimeout(msg) from error
266-
elif isinstance(error, _SSLError) and "timed out" in str(error):
252+
elif isinstance(error, SSLError) and "timed out" in str(error):
267253
# Eventlet does not distinguish TLS network timeouts from other
268254
# SSLErrors (https://github.com/eventlet/eventlet/issues/692).
269255
# Luckily, we can work around this limitation because the phrase
@@ -924,7 +910,7 @@ def _raise_connection_failure(self, error):
924910
reason = ConnectionClosedReason.ERROR
925911
self.close_socket(reason)
926912
# SSLError from PyOpenSSL inherits directly from Exception.
927-
if isinstance(error, (IOError, OSError, _SSLError)):
913+
if isinstance(error, (IOError, OSError, SSLError)):
928914
_raise_connection_failure(self.address, error)
929915
else:
930916
raise
@@ -1024,14 +1010,9 @@ def _configured_socket(address, options):
10241010
if ssl_context is not None:
10251011
host = address[0]
10261012
try:
1027-
# According to RFC6066, section 3, IPv4 and IPv6 literals are
1028-
# not permitted for SNI hostname.
1029-
# Previous to Python 3.7 wrap_socket would blindly pass
1030-
# IP addresses as SNI hostname.
1031-
# https://bugs.python.org/issue32185
10321013
# We have to pass hostname / ip address to wrap_socket
10331014
# to use SSLContext.check_hostname.
1034-
if _HAVE_SNI and (not is_ip_address(host) or _IPADDR_SAFE):
1015+
if HAS_SNI:
10351016
sock = ssl_context.wrap_socket(sock, server_hostname=host)
10361017
else:
10371018
sock = ssl_context.wrap_socket(sock)
@@ -1040,15 +1021,15 @@ def _configured_socket(address, options):
10401021
# Raise _CertificateError directly like we do after match_hostname
10411022
# below.
10421023
raise
1043-
except (IOError, OSError, _SSLError) as exc: # noqa: B014
1024+
except (IOError, OSError, SSLError) as exc: # noqa: B014
10441025
sock.close()
10451026
# We raise AutoReconnect for transient and permanent SSL handshake
10461027
# failures alike. Permanent handshake failures, like protocol
10471028
# mismatch, will be turned into ServerSelectionTimeoutErrors later.
10481029
_raise_connection_failure(address, exc, "SSL handshake failed: ")
10491030
if (
10501031
ssl_context.verify_mode
1051-
and not getattr(ssl_context, "check_hostname", False)
1032+
and not ssl_context.check_hostname
10521033
and not options.tls_allow_invalid_hostnames
10531034
):
10541035
try:
@@ -1336,7 +1317,7 @@ def connect(self):
13361317
self.address, conn_id, ConnectionClosedReason.ERROR
13371318
)
13381319

1339-
if isinstance(error, (IOError, OSError, _SSLError)):
1320+
if isinstance(error, (IOError, OSError, SSLError)):
13401321
_raise_connection_failure(self.address, error)
13411322

13421323
raise

pymongo/pyopenssl_context.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
_REVERSE_VERIFY_MAP = dict((value, key) for key, value in _VERIFY_MAP.items())
7171

7272

73+
# For SNI support. According to RFC6066, section 3, IPv4 and IPv6 literals are
74+
# not permitted for SNI hostname.
7375
def _is_ip_address(address):
7476
try:
7577
_ip_address(address)
@@ -104,8 +106,17 @@ def _call(self, call, *args, **kwargs):
104106
while True:
105107
try:
106108
return call(*args, **kwargs)
107-
except _RETRY_ERRORS:
108-
self.socket_checker.select(self, True, True, timeout)
109+
except _RETRY_ERRORS as exc:
110+
if isinstance(exc, _SSL.WantReadError):
111+
want_read = True
112+
want_write = False
113+
elif isinstance(exc, _SSL.WantWriteError):
114+
want_read = False
115+
want_write = True
116+
else:
117+
want_read = True
118+
want_write = True
119+
self.socket_checker.select(self, want_read, want_write, timeout)
109120
if timeout and _time.monotonic() - start > timeout:
110121
raise _socket.timeout("timed out")
111122
continue

pymongo/ssl_support.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
"""Support for SSL in PyMongo."""
1616

17-
import sys
18-
1917
from pymongo.errors import ConfigurationError
2018

2119
HAVE_SSL = True
@@ -38,7 +36,7 @@
3836
from ssl import CERT_NONE, CERT_REQUIRED
3937

4038
HAS_SNI = _ssl.HAS_SNI
41-
IPADDR_SAFE = _ssl.IS_PYOPENSSL or sys.version_info[:2] >= (3, 7)
39+
IPADDR_SAFE = True
4240
SSLError = _ssl.SSLError
4341

4442
def get_ssl_context(
@@ -53,12 +51,10 @@ def get_ssl_context(
5351
"""Create and return an SSLContext object."""
5452
verify_mode = CERT_NONE if allow_invalid_certificates else CERT_REQUIRED
5553
ctx = _ssl.SSLContext(_ssl.PROTOCOL_SSLv23)
56-
# SSLContext.check_hostname was added in CPython 3.4.
57-
if hasattr(ctx, "check_hostname"):
58-
if verify_mode != CERT_NONE:
59-
ctx.check_hostname = not allow_invalid_hostnames
60-
else:
61-
ctx.check_hostname = False
54+
if verify_mode != CERT_NONE:
55+
ctx.check_hostname = not allow_invalid_hostnames
56+
else:
57+
ctx.check_hostname = False
6258
if hasattr(ctx, "check_ocsp_endpoint"):
6359
ctx.check_ocsp_endpoint = not disable_ocsp_endpoint_check
6460
if hasattr(ctx, "options"):

test/test_encryption.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -145,22 +145,18 @@ def test_init_kms_tls_options(self):
145145
self.assertEqual(opts._kms_ssl_contexts, {})
146146
opts = AutoEncryptionOpts({}, "k.d", kms_tls_options={"kmip": {"tls": True}, "aws": {}})
147147
ctx = opts._kms_ssl_contexts["kmip"]
148-
# On < 3.7 we check hostnames manually.
149-
if sys.version_info[:2] >= (3, 7):
150-
self.assertEqual(ctx.check_hostname, True)
148+
self.assertEqual(ctx.check_hostname, True)
151149
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
152150
ctx = opts._kms_ssl_contexts["aws"]
153-
if sys.version_info[:2] >= (3, 7):
154-
self.assertEqual(ctx.check_hostname, True)
151+
self.assertEqual(ctx.check_hostname, True)
155152
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
156153
opts = AutoEncryptionOpts(
157154
{},
158155
"k.d",
159156
kms_tls_options={"kmip": {"tlsCAFile": CA_PEM, "tlsCertificateKeyFile": CLIENT_PEM}},
160157
)
161158
ctx = opts._kms_ssl_contexts["kmip"]
162-
if sys.version_info[:2] >= (3, 7):
163-
self.assertEqual(ctx.check_hostname, True)
159+
self.assertEqual(ctx.check_hostname, True)
164160
self.assertEqual(ctx.verify_mode, ssl.CERT_REQUIRED)
165161

166162

test/test_ssl.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@
6565
CRL_PEM = os.path.join(CERT_PATH, "crl.pem")
6666
MONGODB_X509_USERNAME = "C=US,ST=New York,L=New York City,O=MDB,OU=Drivers,CN=client"
6767

68-
_PY37PLUS = sys.version_info[:2] >= (3, 7)
69-
7068
# To fully test this start a mongod instance (built with SSL support) like so:
7169
# mongod --dbpath /path/to/data/directory --sslOnNormalPorts \
7270
# --sslPEMKeyFile /path/to/pymongo/test/certificates/server.pem \
@@ -306,10 +304,7 @@ def test_cert_ssl_validation_hostname_matching(self):
306304
ctx = get_ssl_context(None, None, None, None, False, True, False)
307305
self.assertFalse(ctx.check_hostname)
308306
ctx = get_ssl_context(None, None, None, None, False, False, False)
309-
if _PY37PLUS or _HAVE_PYOPENSSL:
310-
self.assertTrue(ctx.check_hostname)
311-
else:
312-
self.assertFalse(ctx.check_hostname)
307+
self.assertTrue(ctx.check_hostname)
313308

314309
response = self.client.admin.command(HelloCompat.LEGACY_CMD)
315310

0 commit comments

Comments
 (0)