2020import io
2121import logging
2222import socket
23+ import ssl
2324import struct
2425import sys
2526from threading import Thread , Event , RLock , Condition
2627import time
27- import ssl
2828import uuid
2929import weakref
3030import random
5050 AuthSuccessMessage , ProtocolException ,
5151 RegisterMessage , ReviseRequestMessage )
5252from cassandra .segment import SegmentCodec , CrcException
53+ from cassandra .tls import (_build_ssl_context_from_options ,
54+ _prepare_ssl_options ,
55+ _ssl_options_requiring_new_context ,
56+ _wrap_socket_from_context )
5357from cassandra .util import OrderedDict
5458from cassandra .shard_info import ShardingInfo # noqa: F401 # re-exported for cassandra.connection.ShardingInfo
5559
@@ -803,6 +807,8 @@ class Connection(object):
803807 endpoint = None
804808 ssl_options = None
805809 ssl_context = None
810+ _ssl_options_explicit = False
811+ _ssl_options_verify_by_default = None
806812 last_error = None
807813
808814 # The current number of operations that are in flight. More precisely,
@@ -858,6 +864,7 @@ class Connection(object):
858864 _socket = None
859865
860866 _socket_impl = socket
867+ _connect_socket_error_types = (socket .error ,)
861868
862869 _check_hostname = False
863870 _product_type = None
@@ -883,9 +890,21 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
883890 on_orphaned_stream_released = None , application_info : Optional [ApplicationInfoBase ] = None ):
884891 # TODO next major rename host to endpoint and remove port kwarg.
885892 self .endpoint = host if isinstance (host , EndPoint ) else DefaultEndPoint (host , port )
893+ endpoint_ssl_options = self .endpoint .ssl_options
894+ self ._endpoint_ssl_options = (
895+ dict (endpoint_ssl_options )
896+ if endpoint_ssl_options is not None
897+ else None
898+ )
886899
887900 self .authenticator = authenticator
888- self .ssl_options = ssl_options .copy () if ssl_options else {}
901+ (
902+ self .ssl_options ,
903+ self ._ssl_options_explicit ,
904+ self ._ssl_options_verify_by_default ,
905+ ) = _prepare_ssl_options (
906+ ssl_options , self ._endpoint_ssl_options
907+ )
889908 self .ssl_context = ssl_context
890909 self .sockopts = sockopts
891910 self .compression = compression
@@ -905,21 +924,28 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
905924 self ._on_orphaned_stream_released = on_orphaned_stream_released
906925 self ._application_info = application_info
907926
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
912-
913- # PYTHON-1331
914- #
915- # We always use SSLContext.wrap_socket() now but legacy configs may have other params that were passed to ssl.wrap_socket()...
916- # and either could have 'check_hostname'. Remove these params into a separate map and use them to build an SSLContext if
917- # we need to do so.
918- #
919- # Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
920- # 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 :
927+ # An explicitly empty options mapping enables TLS with default options.
928+ if self .ssl_context is None and self ._ssl_options_explicit :
922929 self .ssl_context = self ._build_ssl_context_from_options ()
930+ elif self .ssl_context is not None :
931+ endpoint_context_options = _ssl_options_requiring_new_context (
932+ self ._endpoint_ssl_options )
933+ if endpoint_context_options :
934+ # SSLContext has no supported copy operation, and settings such
935+ # as trust roots, client keys, and pyOpenSSL callbacks cannot be
936+ # restored reliably. The cluster context is shared by concurrent
937+ # connections, so mutating and restoring it would leak endpoint
938+ # policy or race with another handshake. Require the existing
939+ # context to remain immutable; callers needing endpoint-specific
940+ # context settings can omit ssl_context and use ssl_options,
941+ # which builds an independent context for every connection.
942+ raise ValueError (
943+ "Endpoint SSL options %s require an independent SSL "
944+ "context and cannot be combined with a supplied "
945+ "ssl_context" % sorted (endpoint_context_options ))
946+
947+ self ._check_hostname = bool (self .ssl_options .get ('check_hostname' , False ) or
948+ getattr (self .ssl_context , 'check_hostname' , False ))
923949
924950 self .max_request_id = min (self .max_in_flight - 1 , (2 ** 15 ) - 1 )
925951 # Don't fill the deque with 2**15 items right away. Start with some and add
@@ -942,6 +968,10 @@ def host(self):
942968 def port (self ):
943969 return self .endpoint .port
944970
971+ @property
972+ def _ssl_enabled (self ):
973+ return self .ssl_context is not None or self ._ssl_options_explicit
974+
945975 @classmethod
946976 def initialize_reactor (cls ):
947977 """
@@ -992,47 +1022,16 @@ def factory(cls, endpoint, timeout, host_conn = None, *args, **kwargs):
9921022 return conn
9931023
9941024 def _build_ssl_context_from_options (self ):
995-
996- # Extract a subset of names from self.ssl_options which apply to SSLContext creation
997- ssl_context_opt_names = ['ssl_version' , 'cert_reqs' , 'check_hostname' , 'keyfile' , 'certfile' , 'ca_certs' , 'ciphers' ]
998- opts = {k :self .ssl_options .get (k , None ) for k in ssl_context_opt_names if k in self .ssl_options }
999-
1000- # Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
1001- # being explicit
1002- ssl_version = opts .get ('ssl_version' , None ) or ssl .PROTOCOL_TLS_CLIENT
1003- cert_reqs = opts .get ('cert_reqs' , None ) or ssl .CERT_REQUIRED
1004- rv = ssl .SSLContext (protocol = int (ssl_version ))
1005- rv .check_hostname = bool (opts .get ('check_hostname' , False ))
1006- rv .options = int (cert_reqs )
1007-
1008- certfile = opts .get ('certfile' , None )
1009- keyfile = opts .get ('keyfile' , None )
1010- if certfile :
1011- rv .load_cert_chain (certfile , keyfile )
1012- ca_certs = opts .get ('ca_certs' , None )
1013- if ca_certs :
1014- rv .load_verify_locations (ca_certs )
1015- ciphers = opts .get ('ciphers' , None )
1016- if ciphers :
1017- rv .set_ciphers (ciphers )
1018-
1019- return rv
1025+ return _build_ssl_context_from_options (
1026+ self .ssl_options ,
1027+ verify_by_default = self ._ssl_options_verify_by_default )
10201028
10211029 def _wrap_socket_from_context (self ):
1022-
1023- # Extract a subset of names from self.ssl_options which apply to SSLContext.wrap_socket (or at least the parts
1024- # of it that don't involve building an SSLContext under the covers)
1025- wrap_socket_opt_names = ['server_side' , 'do_handshake_on_connect' , 'suppress_ragged_eofs' , 'server_hostname' ]
1026- opts = {k :self .ssl_options .get (k , None ) for k in wrap_socket_opt_names if k in self .ssl_options }
1027-
1028- # PYTHON-1186: set the server_hostname only if the SSLContext has
1029- # check_hostname enabled and it is not already provided by the EndPoint ssl options
1030- #opts['server_hostname'] = self.endpoint.address
1031- if (self .ssl_context .check_hostname and 'server_hostname' not in opts ):
1032- server_hostname = self .endpoint .address
1033- opts ['server_hostname' ] = server_hostname
1034-
1035- return self .ssl_context .wrap_socket (self ._socket , ** opts )
1030+ return _wrap_socket_from_context (
1031+ self .ssl_context ,
1032+ self ._socket ,
1033+ self .ssl_options ,
1034+ self .endpoint .address )
10361035
10371036 def _initiate_connection (self , sockaddr ):
10381037 if self .features .shard_id is not None :
@@ -1089,13 +1088,27 @@ def _connect_socket(self):
10891088 self ._validate_hostname ()
10901089 sockerr = None
10911090 break
1092- except socket .error as err :
1091+ except self ._connect_socket_error_types as err :
1092+ if self ._socket :
1093+ self ._socket .close ()
1094+ self ._socket = None
1095+ # Preserve a TLS or driver error over later socket failures so
1096+ # a final ECONNREFUSED cannot hide certificate diagnostics.
1097+ if (sockerr is None or
1098+ (isinstance (sockerr , socket .error ) and
1099+ not isinstance (sockerr , ssl .SSLError ))):
1100+ sockerr = err
1101+ except Exception :
10931102 if self ._socket :
10941103 self ._socket .close ()
10951104 self ._socket = None
1096- sockerr = err
1105+ raise
10971106
10981107 if sockerr :
1108+ if isinstance (sockerr , ssl .SSLError ):
1109+ raise sockerr
1110+ if not isinstance (sockerr , socket .error ):
1111+ raise sockerr
10991112 raise socket .error (sockerr .errno , "Tried connecting to %s. Last error: %s" %
11001113 ([a [4 ] for a in addresses ], sockerr .strerror or sockerr ))
11011114
0 commit comments