Skip to content

Commit d77f7f0

Browse files
committed
connection: preserve explicit empty ssl_options
1 parent bcc2d3d commit d77f7f0

16 files changed

Lines changed: 605 additions & 59 deletions

cassandra/cluster.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,7 +1298,8 @@ def __init__(self,
12981298

12991299
if cloud is not None:
13001300
self.cloud = cloud
1301-
if contact_points is not _NOT_SET or endpoint_factory or ssl_context or ssl_options:
1301+
if (contact_points is not _NOT_SET or endpoint_factory or
1302+
ssl_context is not None or ssl_options is not None):
13021303
raise ValueError("contact_points, endpoint_factory, ssl_context, and ssl_options "
13031304
"cannot be specified with a cloud configuration")
13041305

@@ -1508,7 +1509,7 @@ def __init__(self,
15081509

15091510
self.metrics_enabled = metrics_enabled
15101511

1511-
if ssl_options and not ssl_context:
1512+
if ssl_options is not None and ssl_context is None:
15121513
warn('Using ssl_options without ssl_context is '
15131514
'deprecated and will result in an error in '
15141515
'the next major release. Please use ssl_context '

cassandra/connection.py

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555

5656
log = logging.getLogger(__name__)
5757

58+
5859
segment_codec_no_compression = SegmentCodec()
5960
segment_codec_lz4 = None
6061

@@ -130,6 +131,81 @@ def decompress(byts):
130131
frame_header_v3 = struct.Struct('>BhBi')
131132

132133

134+
def _default_pyopenssl_ssl_method(ssl_module):
135+
for method_name in ('TLS_CLIENT_METHOD', 'TLS_METHOD', 'TLSv1_2_METHOD'):
136+
method = getattr(ssl_module, method_name, None)
137+
if method is not None:
138+
return method
139+
raise ImportError('pyOpenSSL does not expose a secure TLS client method')
140+
141+
142+
def _pyopenssl_ssl_method_from_stdlib(ssl_module, ssl_version):
143+
if ssl_version is None:
144+
return _default_pyopenssl_ssl_method(ssl_module)
145+
146+
protocol_method_names = (
147+
('PROTOCOL_TLS_CLIENT', ('TLS_CLIENT_METHOD', 'TLS_METHOD', 'TLSv1_2_METHOD')),
148+
('PROTOCOL_TLS', ('TLS_METHOD', 'TLS_CLIENT_METHOD', 'TLSv1_2_METHOD')),
149+
('PROTOCOL_SSLv23', ('TLS_METHOD', 'TLS_CLIENT_METHOD', 'TLSv1_2_METHOD')),
150+
('PROTOCOL_TLSv1_2', ('TLSv1_2_METHOD',)),
151+
('PROTOCOL_TLSv1_1', ('TLSv1_1_METHOD',)),
152+
('PROTOCOL_TLSv1', ('TLSv1_METHOD',)),
153+
)
154+
for protocol_name, method_names in protocol_method_names:
155+
protocol = getattr(ssl, protocol_name, None)
156+
if (protocol is not None and
157+
ssl_version.__class__ is protocol.__class__ and
158+
ssl_version == protocol):
159+
for method_name in method_names:
160+
method = getattr(ssl_module, method_name, None)
161+
if method is not None:
162+
return method
163+
raise ImportError('pyOpenSSL does not expose a method for %s' % (protocol_name,))
164+
165+
return ssl_version
166+
167+
168+
def _pyopenssl_verify_mode_from_cert_reqs(ssl_module, cert_reqs):
169+
if cert_reqs is None:
170+
return None
171+
if cert_reqs.__class__ is not type(ssl.CERT_REQUIRED):
172+
return cert_reqs
173+
if cert_reqs == ssl.CERT_NONE:
174+
return ssl_module.VERIFY_NONE
175+
if cert_reqs in (ssl.CERT_OPTIONAL, ssl.CERT_REQUIRED):
176+
return ssl_module.VERIFY_PEER
177+
return cert_reqs
178+
179+
180+
def _build_pyopenssl_context_from_options(ssl_module, ssl_options):
181+
ssl_options = ssl_options or {}
182+
context = ssl_module.Context(
183+
_pyopenssl_ssl_method_from_stdlib(ssl_module, ssl_options.get('ssl_version', None))
184+
)
185+
if 'certfile' in ssl_options:
186+
context.use_certificate_file(ssl_options['certfile'])
187+
if 'keyfile' in ssl_options:
188+
context.use_privatekey_file(ssl_options['keyfile'])
189+
if 'ca_certs' in ssl_options:
190+
context.load_verify_locations(ssl_options['ca_certs'])
191+
cert_reqs = _pyopenssl_verify_mode_from_cert_reqs(
192+
ssl_module, ssl_options.get('cert_reqs', None))
193+
if cert_reqs is None:
194+
cert_reqs = (ssl_module.VERIFY_PEER
195+
if (ssl_options.get('ca_certs', None) or ssl_options.get('check_hostname', False))
196+
else ssl_module.VERIFY_NONE)
197+
context.set_verify(
198+
cert_reqs,
199+
callback=lambda _connection, _x509, _errnum, _errdepth, ok: ok
200+
)
201+
ciphers = ssl_options.get('ciphers', None)
202+
if ciphers:
203+
if isinstance(ciphers, str):
204+
ciphers = ciphers.encode('ascii')
205+
context.set_cipher_list(ciphers)
206+
return context
207+
208+
133209
class EndPoint(object):
134210
"""
135211
Represents the information to connect to a cassandra node.
@@ -803,6 +879,7 @@ class Connection(object):
803879
endpoint = None
804880
ssl_options = None
805881
ssl_context = None
882+
_ssl_options_explicit = False
806883
last_error = None
807884

808885
# The current number of operations that are in flight. More precisely,
@@ -885,7 +962,11 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
885962
self.endpoint = host if isinstance(host, EndPoint) else DefaultEndPoint(host, port)
886963

887964
self.authenticator = authenticator
888-
self.ssl_options = ssl_options.copy() if ssl_options else {}
965+
endpoint_ssl_options = self.endpoint.ssl_options
966+
# Explicit ssl_options={} enables SSL with default options; omitted
967+
# ssl_options=None leaves SSL disabled unless an endpoint supplies options.
968+
self._ssl_options_explicit = ssl_options is not None
969+
self.ssl_options = ssl_options.copy() if ssl_options is not None else {}
889970
self.ssl_context = ssl_context
890971
self.sockopts = sockopts
891972
self.compression = compression
@@ -905,10 +986,13 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
905986
self._on_orphaned_stream_released = on_orphaned_stream_released
906987
self._application_info = application_info
907988

908-
if ssl_options:
909-
self.ssl_options.update(self.endpoint.ssl_options or {})
910-
elif self.endpoint.ssl_options:
911-
self.ssl_options = self.endpoint.ssl_options
989+
if ssl_options is not None:
990+
self.ssl_options.update(endpoint_ssl_options or {})
991+
elif endpoint_ssl_options is not None:
992+
self._ssl_options_explicit = True
993+
self.ssl_options = endpoint_ssl_options
994+
self._check_hostname = bool(self.ssl_options.get('check_hostname', False) or
995+
getattr(self.ssl_context, 'check_hostname', False))
912996

913997
# PYTHON-1331
914998
#
@@ -918,7 +1002,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9181002
#
9191003
# Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
9201004
# operation ssl_options should contain only args needed for the ssl_context.wrap_socket() call.
921-
if not self.ssl_context and self.ssl_options:
1005+
if self.ssl_context is None and self._ssl_options_explicit:
9221006
self.ssl_context = self._build_ssl_context_from_options()
9231007

9241008
self.max_request_id = min(self.max_in_flight - 1, (2 ** 15) - 1)
@@ -942,6 +1026,10 @@ def host(self):
9421026
def port(self):
9431027
return self.endpoint.port
9441028

1029+
@property
1030+
def _ssl_enabled(self):
1031+
return self.ssl_context is not None or self._ssl_options_explicit
1032+
9451033
@classmethod
9461034
def initialize_reactor(cls):
9471035
"""
@@ -1000,10 +1088,15 @@ def _build_ssl_context_from_options(self):
10001088
# Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
10011089
# being explicit
10021090
ssl_version = opts.get('ssl_version', None) or ssl.PROTOCOL_TLS_CLIENT
1003-
cert_reqs = opts.get('cert_reqs', None) or ssl.CERT_REQUIRED
1091+
cert_reqs = opts.get('cert_reqs', None)
1092+
if cert_reqs is None:
1093+
cert_reqs = (ssl.CERT_REQUIRED
1094+
if (opts.get('ca_certs', None) or opts.get('check_hostname', False))
1095+
else ssl.CERT_NONE)
10041096
rv = ssl.SSLContext(protocol=int(ssl_version))
1097+
rv.check_hostname = False
1098+
rv.verify_mode = cert_reqs
10051099
rv.check_hostname = bool(opts.get('check_hostname', False))
1006-
rv.options = int(cert_reqs)
10071100

10081101
certfile = opts.get('certfile', None)
10091102
keyfile = opts.get('keyfile', None)

cassandra/datastax/cloud/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from zipfile import BadZipfile as BadZipFile
3535

3636
from cassandra import DriverException
37+
from cassandra.connection import _default_pyopenssl_ssl_method
3738

3839
log = logging.getLogger(__name__)
3940

@@ -183,10 +184,10 @@ def _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location):
183184
raise ImportError(
184185
"PyOpenSSL must be installed to connect to Astra with the Eventlet or Twisted event loops")\
185186
.with_traceback(e.__traceback__)
186-
ssl_context = SSL.Context(SSL.TLSv1_METHOD)
187+
ssl_context = SSL.Context(_default_pyopenssl_ssl_method(SSL))
187188
ssl_context.set_verify(SSL.VERIFY_PEER, callback=lambda _1, _2, _3, _4, ok: ok)
188189
ssl_context.use_certificate_file(cert_location)
189190
ssl_context.use_privatekey_file(key_location)
190191
ssl_context.load_verify_locations(ca_cert_location)
191192

192-
return ssl_context
193+
return ssl_context

cassandra/datastax/insights/reporter.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@
3333
log = logging.getLogger(__name__)
3434

3535

36+
def _ssl_options_cert_validation_enabled(ssl_options):
37+
cert_reqs = ssl_options.get('cert_reqs')
38+
if cert_reqs is not None:
39+
return cert_reqs != ssl.CERT_NONE
40+
return bool(ssl_options.get('ca_certs') or ssl_options.get('check_hostname', False))
41+
42+
3643
class MonitorReporter(Thread):
3744

3845
def __init__(self, interval_sec, session):
@@ -141,14 +148,14 @@ def _get_startup_data(self):
141148

142149
cert_validation = None
143150
try:
144-
if self._session.cluster.ssl_context:
151+
if self._session.cluster.ssl_context is not None:
145152
if isinstance(self._session.cluster.ssl_context, ssl.SSLContext):
146153
cert_validation = self._session.cluster.ssl_context.verify_mode == ssl.CERT_REQUIRED
147154
else: # pyopenssl
148155
from OpenSSL import SSL
149156
cert_validation = self._session.cluster.ssl_context.get_verify_mode() != SSL.VERIFY_NONE
150-
elif self._session.cluster.ssl_options:
151-
cert_validation = self._session.cluster.ssl_options.get('cert_reqs') == ssl.CERT_REQUIRED
157+
elif self._session.cluster.ssl_options is not None:
158+
cert_validation = _ssl_options_cert_validation_enabled(self._session.cluster.ssl_options)
152159
except Exception as e:
153160
log.debug('Unable to get the cert validation: {}'.format(e))
154161

@@ -186,7 +193,8 @@ def _get_startup_data(self):
186193
'compression': compression_type.upper() if compression_type else 'NONE',
187194
'reconnectionPolicy': insights_registry.serialize(self._session.cluster.reconnection_policy),
188195
'sslConfigured': {
189-
'enabled': bool(self._session.cluster.ssl_options or self._session.cluster.ssl_context),
196+
'enabled': (self._session.cluster.ssl_context is not None or
197+
self._session.cluster.ssl_options is not None),
190198
'certValidation': cert_validation
191199
},
192200
'authProvider': {

cassandra/io/asyncioreactor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ def __init__(self, *args, **kwargs):
133133
Connection.__init__(self, *args, **kwargs)
134134
self._background_tasks = set()
135135
self._transport = None
136-
self._using_ssl = bool(self.ssl_context)
136+
self._using_ssl = self._ssl_enabled
137137

138138
self._connect_socket()
139139
self._socket.setblocking(0)

cassandra/io/eventletreactor.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
from threading import Event
2424
import time
2525

26-
from cassandra.connection import Connection, ConnectionShutdown, Timer, TimerManager
26+
from cassandra.connection import (Connection, ConnectionShutdown, Timer, TimerManager,
27+
_build_pyopenssl_context_from_options as _build_pyopenssl_context,
28+
_default_pyopenssl_ssl_method)
2729
try:
2830
from eventlet.green.OpenSSL import SSL
2931
_PYOPENSSL = True
@@ -35,6 +37,10 @@
3537
log = logging.getLogger(__name__)
3638

3739

40+
def _default_ssl_method():
41+
return _default_pyopenssl_ssl_method(SSL)
42+
43+
3844
def _check_pyopenssl():
3945
if not _PYOPENSSL:
4046
raise ImportError(
@@ -43,6 +49,10 @@ def _check_pyopenssl():
4349
)
4450

4551

52+
def _build_pyopenssl_context_from_options(ssl_options):
53+
return _build_pyopenssl_context(SSL, ssl_options)
54+
55+
4656
class EventletConnection(Connection):
4757
"""
4858
An implementation of :class:`.Connection` that utilizes ``eventlet``.
@@ -92,7 +102,7 @@ def service_timeouts(cls):
92102

93103
def __init__(self, *args, **kwargs):
94104
Connection.__init__(self, *args, **kwargs)
95-
self.uses_legacy_ssl_options = self.ssl_options and not self.ssl_context
105+
self.uses_legacy_ssl_options = False
96106
self._write_queue = Queue()
97107

98108
self._connect_socket()
@@ -108,24 +118,29 @@ def _wrap_socket_from_context(self):
108118
if self.ssl_options and 'server_hostname' in self.ssl_options:
109119
# This is necessary for SNI
110120
self._socket.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
121+
return self._socket
111122

112123
def _initiate_connection(self, sockaddr):
113124
if self.uses_legacy_ssl_options:
114125
super(EventletConnection, self)._initiate_connection(sockaddr)
115126
else:
116127
self._socket.connect(sockaddr)
117-
if self.ssl_context or self.ssl_options:
128+
if self._ssl_enabled:
118129
self._socket.do_handshake()
119130

120-
def _match_hostname(self):
131+
def _validate_hostname(self):
121132
if self.uses_legacy_ssl_options:
122-
super(EventletConnection, self)._match_hostname()
133+
super(EventletConnection, self)._validate_hostname()
123134
else:
124135
cert_name = self._socket.get_peer_certificate().get_subject().commonName
125136
if cert_name != self.endpoint.address:
126137
raise Exception("Hostname verification failed! Certificate name '{}' "
127138
"doesn't endpoint '{}'".format(cert_name, self.endpoint.address))
128139

140+
def _build_ssl_context_from_options(self):
141+
_check_pyopenssl()
142+
return _build_pyopenssl_context_from_options(self.ssl_options)
143+
129144
def close(self):
130145
with self.lock:
131146
if self.is_closed:

0 commit comments

Comments
 (0)