Skip to content

Commit 8ff0ba0

Browse files
authored
PYTHON-1331 ssl.match_hostname() is deprecated in 3.7 (#1191)
1 parent e90c0f5 commit 8ff0ba0

File tree

6 files changed

+91
-40
lines changed

6 files changed

+91
-40
lines changed

Jenkinsfile

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ def initializeEnvironment() {
178178
// Determine if server version is Apache CassandraⓇ or DataStax Enterprise
179179
if (env.CASSANDRA_VERSION.split('-')[0] == 'dse') {
180180
if (env.PYTHON_VERSION =~ /3\.12\.\d+/) {
181-
echo "Cannot install DSE dependencies for Python 3.12.x. See PYTHON-1368 for more detail."
181+
echo "Cannot install DSE dependencies for Python 3.12.x; installing Apache CassandraⓇ requirements only. See PYTHON-1368 for more detail."
182+
sh label: 'Install Apache CassandraⓇ requirements', script: '''#!/bin/bash -lex
183+
pip install -r test-requirements.txt
184+
'''
182185
}
183186
else {
184187
sh label: 'Install DataStax Enterprise requirements', script: '''#!/bin/bash -lex
@@ -196,7 +199,8 @@ def initializeEnvironment() {
196199
}
197200

198201
sh label: 'Install unit test modules', script: '''#!/bin/bash -lex
199-
pip install pynose nose-ignore-docstring nose-exclude service_identity
202+
pip install --no-deps nose-ignore-docstring nose-exclude
203+
pip install service_identity
200204
'''
201205

202206
if (env.CYTHON_ENABLED == 'True') {

cassandra/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ class DependencyException(Exception):
742742
def __init__(self, msg, excs=[]):
743743
complete_msg = msg
744744
if excs:
745-
complete_msg += ("The following exceptions were observed: \n" + '\n'.join(str(e) for e in excs))
745+
complete_msg += ("\nThe following exceptions were observed: \n - " + '\n - '.join(str(e) for e in excs))
746746
Exception.__init__(self, complete_msg)
747747

748748
class VectorDeserializationFailure(DriverException):

cassandra/cluster.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,10 +165,12 @@ def _connection_reduce_fn(val,import_fn):
165165
excs.append(exc)
166166
return (rv or import_result, excs)
167167

168+
log = logging.getLogger(__name__)
169+
168170
conn_fns = (_try_gevent_import, _try_eventlet_import, _try_libev_import, _try_asyncore_import)
169171
(conn_class, excs) = reduce(_connection_reduce_fn, conn_fns, (None,[]))
170-
if excs:
171-
raise DependencyException("Exception loading connection class dependencies", excs)
172+
if not conn_class:
173+
raise DependencyException("Unable to load a default connection class", excs)
172174
DefaultConnection = conn_class
173175

174176
# Forces load of utf8 encoding module to avoid deadlock that occurs
@@ -177,8 +179,6 @@ def _connection_reduce_fn(val,import_fn):
177179
# See http://bugs.python.org/issue10923
178180
"".encode('utf8')
179181

180-
log = logging.getLogger(__name__)
181-
182182

183183
DEFAULT_MIN_REQUESTS = 5
184184
DEFAULT_MAX_REQUESTS = 100
@@ -811,9 +811,9 @@ def default_retry_policy(self, policy):
811811
Using ssl_options without ssl_context is deprecated and will be removed in the
812812
next major release.
813813
814-
An optional dict which will be used as kwargs for ``ssl.SSLContext.wrap_socket`` (or
815-
``ssl.wrap_socket()`` if used without ssl_context) when new sockets are created.
816-
This should be used when client encryption is enabled in Cassandra.
814+
An optional dict which will be used as kwargs for ``ssl.SSLContext.wrap_socket``
815+
when new sockets are created. This should be used when client encryption is enabled
816+
in Cassandra.
817817
818818
The following documentation only applies when ssl_options is used without ssl_context.
819819
@@ -829,6 +829,12 @@ def default_retry_policy(self, policy):
829829
should almost always require the option ``'cert_reqs': ssl.CERT_REQUIRED``. Note also that this functionality was not built into
830830
Python standard library until (2.7.9, 3.2). To enable this mechanism in earlier versions, patch ``ssl.match_hostname``
831831
with a custom or `back-ported function <https://pypi.org/project/backports.ssl_match_hostname/>`_.
832+
833+
.. versionchanged:: 3.29.0
834+
835+
``ssl.match_hostname`` has been deprecated since Python 3.7 (and removed in Python 3.12). This functionality is now implemented
836+
via ``ssl.SSLContext.check_hostname``. All options specified above (including ``check_hostname``) should continue to behave in a
837+
way that is consistent with prior implementations.
832838
"""
833839

834840
ssl_context = None

cassandra/connection.py

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,6 @@ class Connection(object):
733733
_socket = None
734734

735735
_socket_impl = socket
736-
_ssl_impl = ssl
737736

738737
_check_hostname = False
739738
_product_type = None
@@ -757,7 +756,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
757756
self.endpoint = host if isinstance(host, EndPoint) else DefaultEndPoint(host, port)
758757

759758
self.authenticator = authenticator
760-
self.ssl_options = ssl_options.copy() if ssl_options else None
759+
self.ssl_options = ssl_options.copy() if ssl_options else {}
761760
self.ssl_context = ssl_context
762761
self.sockopts = sockopts
763762
self.compression = compression
@@ -777,15 +776,20 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
777776
self._on_orphaned_stream_released = on_orphaned_stream_released
778777

779778
if ssl_options:
780-
self._check_hostname = bool(self.ssl_options.pop('check_hostname', False))
781-
if self._check_hostname:
782-
if not getattr(ssl, 'match_hostname', None):
783-
raise RuntimeError("ssl_options specify 'check_hostname', but ssl.match_hostname is not provided. "
784-
"Patch or upgrade Python to use this option.")
785779
self.ssl_options.update(self.endpoint.ssl_options or {})
786780
elif self.endpoint.ssl_options:
787781
self.ssl_options = self.endpoint.ssl_options
788782

783+
# PYTHON-1331
784+
#
785+
# We always use SSLContext.wrap_socket() now but legacy configs may have other params that were passed to ssl.wrap_socket()...
786+
# and either could have 'check_hostname'. Remove these params into a separate map and use them to build an SSLContext if
787+
# we need to do so.
788+
#
789+
# Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
790+
# operation ssl_options should contain only args needed for the ssl_context.wrap_socket() call.
791+
if not self.ssl_context and self.ssl_options:
792+
self.ssl_context = self._build_ssl_context_from_options()
789793

790794
if protocol_version >= 3:
791795
self.max_request_id = min(self.max_in_flight - 1, (2 ** 15) - 1)
@@ -852,21 +856,57 @@ def factory(cls, endpoint, timeout, *args, **kwargs):
852856
else:
853857
return conn
854858

859+
def _build_ssl_context_from_options(self):
860+
861+
# Extract a subset of names from self.ssl_options which apply to SSLContext creation
862+
ssl_context_opt_names = ['ssl_version', 'cert_reqs', 'check_hostname', 'keyfile', 'certfile', 'ca_certs', 'ciphers']
863+
opts = {k:self.ssl_options.get(k, None) for k in ssl_context_opt_names if k in self.ssl_options}
864+
865+
# Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
866+
# being explicit
867+
ssl_version = opts.get('ssl_version', None) or ssl.PROTOCOL_TLS_CLIENT
868+
cert_reqs = opts.get('cert_reqs', None) or ssl.CERT_REQUIRED
869+
rv = ssl.SSLContext(protocol=int(ssl_version))
870+
rv.check_hostname = bool(opts.get('check_hostname', False))
871+
rv.options = int(cert_reqs)
872+
873+
certfile = opts.get('certfile', None)
874+
keyfile = opts.get('keyfile', None)
875+
if certfile:
876+
rv.load_cert_chain(certfile, keyfile)
877+
ca_certs = opts.get('ca_certs', None)
878+
if ca_certs:
879+
rv.load_verify_locations(ca_certs)
880+
ciphers = opts.get('ciphers', None)
881+
if ciphers:
882+
rv.set_ciphers(ciphers)
883+
884+
return rv
885+
855886
def _wrap_socket_from_context(self):
856-
ssl_options = self.ssl_options or {}
887+
888+
# Extract a subset of names from self.ssl_options which apply to SSLContext.wrap_socket (or at least the parts
889+
# of it that don't involve building an SSLContext under the covers)
890+
wrap_socket_opt_names = ['server_side', 'do_handshake_on_connect', 'suppress_ragged_eofs', 'server_hostname']
891+
opts = {k:self.ssl_options.get(k, None) for k in wrap_socket_opt_names if k in self.ssl_options}
892+
857893
# PYTHON-1186: set the server_hostname only if the SSLContext has
858894
# check_hostname enabled and it is not already provided by the EndPoint ssl options
859-
if (self.ssl_context.check_hostname and
860-
'server_hostname' not in ssl_options):
861-
ssl_options = ssl_options.copy()
862-
ssl_options['server_hostname'] = self.endpoint.address
863-
self._socket = self.ssl_context.wrap_socket(self._socket, **ssl_options)
895+
#opts['server_hostname'] = self.endpoint.address
896+
if (self.ssl_context.check_hostname and 'server_hostname' not in opts):
897+
server_hostname = self.endpoint.address
898+
opts['server_hostname'] = server_hostname
899+
900+
return self.ssl_context.wrap_socket(self._socket, **opts)
864901

865902
def _initiate_connection(self, sockaddr):
866903
self._socket.connect(sockaddr)
867904

868-
def _match_hostname(self):
869-
ssl.match_hostname(self._socket.getpeercert(), self.endpoint.address)
905+
# PYTHON-1331
906+
#
907+
# Allow implementations specific to an event loop to add additional behaviours
908+
def _validate_hostname(self):
909+
pass
870910

871911
def _get_socket_addresses(self):
872912
address, port = self.endpoint.resolve()
@@ -887,16 +927,18 @@ def _connect_socket(self):
887927
try:
888928
self._socket = self._socket_impl.socket(af, socktype, proto)
889929
if self.ssl_context:
890-
self._wrap_socket_from_context()
891-
elif self.ssl_options:
892-
if not self._ssl_impl:
893-
raise RuntimeError("This version of Python was not compiled with SSL support")
894-
self._socket = self._ssl_impl.wrap_socket(self._socket, **self.ssl_options)
930+
self._socket = self._wrap_socket_from_context()
895931
self._socket.settimeout(self.connect_timeout)
896932
self._initiate_connection(sockaddr)
897933
self._socket.settimeout(None)
934+
935+
# PYTHON-1331
936+
#
937+
# Most checking is done via the check_hostname param on the SSLContext.
938+
# Subclasses can add additional behaviours via _validate_hostname() so
939+
# run that here.
898940
if self._check_hostname:
899-
self._match_hostname()
941+
self._validate_hostname()
900942
sockerr = None
901943
break
902944
except socket.error as err:

cassandra/io/eventletreactor.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -103,11 +103,12 @@ def __init__(self, *args, **kwargs):
103103

104104
def _wrap_socket_from_context(self):
105105
_check_pyopenssl()
106-
self._socket = SSL.Connection(self.ssl_context, self._socket)
107-
self._socket.set_connect_state()
106+
rv = SSL.Connection(self.ssl_context, self._socket)
107+
rv.set_connect_state()
108108
if self.ssl_options and 'server_hostname' in self.ssl_options:
109109
# This is necessary for SNI
110-
self._socket.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
110+
rv.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
111+
return rv
111112

112113
def _initiate_connection(self, sockaddr):
113114
if self.uses_legacy_ssl_options:
@@ -117,14 +118,12 @@ def _initiate_connection(self, sockaddr):
117118
if self.ssl_context or self.ssl_options:
118119
self._socket.do_handshake()
119120

120-
def _match_hostname(self):
121-
if self.uses_legacy_ssl_options:
122-
super(EventletConnection, self)._match_hostname()
123-
else:
121+
def _validate_hostname(self):
122+
if not self.uses_legacy_ssl_options:
124123
cert_name = self._socket.get_peer_certificate().get_subject().commonName
125124
if cert_name != self.endpoint.address:
126125
raise Exception("Hostname verification failed! Certificate name '{}' "
127-
"doesn't endpoint '{}'".format(cert_name, self.endpoint.address))
126+
"doesn't match endpoint '{}'".format(cert_name, self.endpoint.address))
128127

129128
def close(self):
130129
with self.lock:

test-requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
-r requirements.txt
22
scales
3-
nose
3+
pynose
44
mock>1.1
55
ccm>=2.1.2
66
pytz

0 commit comments

Comments
 (0)