1818from functools import wraps , partial , total_ordering
1919from heapq import heappush , heappop
2020import io
21+ import ipaddress
2122import logging
2223import socket
2324import struct
5556
5657log = logging .getLogger (__name__ )
5758
59+
60+ def _pyopenssl_cert_to_ssl_cert (peer_cert ):
61+ from cryptography import x509
62+ from cryptography .x509 .oid import NameOID
63+
64+ cert = peer_cert .to_cryptography ()
65+ ssl_cert = {}
66+
67+ subject = tuple ((('commonName' , attr .value ),)
68+ for attr in cert .subject .get_attributes_for_oid (NameOID .COMMON_NAME ))
69+ if subject :
70+ ssl_cert ['subject' ] = subject
71+
72+ subject_alt_names = []
73+ try :
74+ san = cert .extensions .get_extension_for_class (x509 .SubjectAlternativeName ).value
75+ except x509 .ExtensionNotFound :
76+ pass
77+ else :
78+ subject_alt_names .extend (('DNS' , name ) for name in san .get_values_for_type (x509 .DNSName ))
79+ subject_alt_names .extend (('IP Address' , str (address ))
80+ for address in san .get_values_for_type (x509 .IPAddress ))
81+
82+ if subject_alt_names :
83+ ssl_cert ['subjectAltName' ] = tuple (subject_alt_names )
84+
85+ return ssl_cert
86+
87+
88+ def _validate_pyopenssl_hostname (peer_cert , hostname ):
89+ cert = _pyopenssl_cert_to_ssl_cert (peer_cert )
90+ try :
91+ host_ip = ipaddress .ip_address (hostname )
92+ except ValueError :
93+ host_ip = None
94+
95+ names = []
96+ for key , value in cert .get ('subjectAltName' , ()):
97+ if key == 'DNS' :
98+ if host_ip is None and _dnsname_match (value , hostname ):
99+ return
100+ names .append (value )
101+ elif key == 'IP Address' :
102+ if host_ip is not None and _ipaddress_match (value , host_ip ):
103+ return
104+ names .append (value )
105+
106+ if not names :
107+ for subject in cert .get ('subject' , ()):
108+ for key , value in subject :
109+ if key == 'commonName' :
110+ if _dnsname_match (value , hostname ):
111+ return
112+ names .append (value )
113+
114+ if len (names ) > 1 :
115+ raise ssl .CertificateError ("hostname {!r} doesn't match either of {}" .format (
116+ hostname , ', ' .join (map (repr , names ))))
117+ if len (names ) == 1 :
118+ raise ssl .CertificateError ("hostname {!r} doesn't match {!r}" .format (hostname , names [0 ]))
119+ raise ssl .CertificateError ("no appropriate commonName or subjectAltName fields were found" )
120+
121+
122+ def _dnsname_match (pattern , hostname ):
123+ if not pattern :
124+ return False
125+
126+ wildcards = pattern .count ('*' )
127+ if not wildcards :
128+ return pattern .lower () == hostname .lower ()
129+ if wildcards > 1 :
130+ raise ssl .CertificateError (
131+ "too many wildcards in certificate DNS name: {!r}." .format (pattern ))
132+
133+ leftmost , sep , remainder = pattern .partition ('.' )
134+ if '*' in remainder :
135+ raise ssl .CertificateError (
136+ "wildcard can only be present in the leftmost label: {!r}." .format (pattern ))
137+ if not sep :
138+ raise ssl .CertificateError (
139+ "sole wildcard without additional labels is not supported: {!r}." .format (pattern ))
140+ if leftmost != '*' :
141+ raise ssl .CertificateError (
142+ "partial wildcards in leftmost label are not supported: {!r}." .format (pattern ))
143+
144+ hostname_leftmost , sep , hostname_remainder = hostname .partition ('.' )
145+ if not hostname_leftmost or not sep :
146+ return False
147+ return remainder .lower () == hostname_remainder .lower ()
148+
149+
150+ def _ipaddress_match (pattern , host_ip ):
151+ try :
152+ return ipaddress .ip_address (pattern ) == host_ip
153+ except ValueError :
154+ return False
155+
156+
58157segment_codec_no_compression = SegmentCodec ()
59158segment_codec_lz4 = None
60159
@@ -803,6 +902,7 @@ class Connection(object):
803902 endpoint = None
804903 ssl_options = None
805904 ssl_context = None
905+ _ssl_options_explicit = False
806906 last_error = None
807907
808908 # The current number of operations that are in flight. More precisely,
@@ -885,7 +985,11 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
885985 self .endpoint = host if isinstance (host , EndPoint ) else DefaultEndPoint (host , port )
886986
887987 self .authenticator = authenticator
888- self .ssl_options = ssl_options .copy () if ssl_options else {}
988+ endpoint_ssl_options = self .endpoint .ssl_options
989+ # Explicit ssl_options={} enables SSL with default options; omitted
990+ # ssl_options=None leaves SSL disabled unless an endpoint supplies options.
991+ self ._ssl_options_explicit = ssl_options is not None
992+ self .ssl_options = ssl_options .copy () if ssl_options is not None else {}
889993 self .ssl_context = ssl_context
890994 self .sockopts = sockopts
891995 self .compression = compression
@@ -905,10 +1009,13 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9051009 self ._on_orphaned_stream_released = on_orphaned_stream_released
9061010 self ._application_info = application_info
9071011
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
1012+ if ssl_options is not None :
1013+ self .ssl_options .update (endpoint_ssl_options or {})
1014+ elif endpoint_ssl_options is not None :
1015+ self ._ssl_options_explicit = True
1016+ self .ssl_options = endpoint_ssl_options
1017+ self ._check_hostname = bool (self .ssl_options .get ('check_hostname' , False ) or
1018+ getattr (self .ssl_context , 'check_hostname' , False ))
9121019
9131020 # PYTHON-1331
9141021 #
@@ -918,7 +1025,7 @@ def __init__(self, host='127.0.0.1', port=9042, authenticator=None,
9181025 #
9191026 # Note the use of pop() here; we are very deliberately removing these params from ssl_options if they're present. After this
9201027 # 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 :
1028+ if self .ssl_context is None and self ._ssl_options_explicit :
9221029 self .ssl_context = self ._build_ssl_context_from_options ()
9231030
9241031 self .max_request_id = min (self .max_in_flight - 1 , (2 ** 15 ) - 1 )
@@ -942,6 +1049,10 @@ def host(self):
9421049 def port (self ):
9431050 return self .endpoint .port
9441051
1052+ @property
1053+ def _ssl_enabled (self ):
1054+ return self .ssl_context is not None or self ._ssl_options_explicit
1055+
9451056 @classmethod
9461057 def initialize_reactor (cls ):
9471058 """
@@ -1000,10 +1111,15 @@ def _build_ssl_context_from_options(self):
10001111 # Python >= 3.10 requires either PROTOCOL_TLS_CLIENT or PROTOCOL_TLS_SERVER so we'll get ahead of things by always
10011112 # being explicit
10021113 ssl_version = opts .get ('ssl_version' , None ) or ssl .PROTOCOL_TLS_CLIENT
1003- cert_reqs = opts .get ('cert_reqs' , None ) or ssl .CERT_REQUIRED
1114+ cert_reqs = opts .get ('cert_reqs' , None )
1115+ if cert_reqs is None :
1116+ cert_reqs = (ssl .CERT_REQUIRED
1117+ if (opts .get ('ca_certs' , None ) or opts .get ('check_hostname' , False ))
1118+ else ssl .CERT_NONE )
10041119 rv = ssl .SSLContext (protocol = int (ssl_version ))
1120+ rv .check_hostname = False
1121+ rv .verify_mode = cert_reqs
10051122 rv .check_hostname = bool (opts .get ('check_hostname' , False ))
1006- rv .options = int (cert_reqs )
10071123
10081124 certfile = opts .get ('certfile' , None )
10091125 keyfile = opts .get ('keyfile' , None )
0 commit comments