Skip to content

Commit 5f13adf

Browse files
[PYTHON-1162] ssl context and cloud support for Eventlet (datastax#1051)
1 parent aaa9226 commit 5f13adf

File tree

9 files changed

+90
-28
lines changed

9 files changed

+90
-28
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Unreleased
55
Features
66
--------
77
* Allow passing ssl context for Twisted (PYTHON-1161)
8+
* ssl context and cloud support for Eventlet (PYTHON-1162)
89
* Cloud Twisted support (PYTHON-1163)
910

1011
3.20.1

build.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ build:
227227
"tests/integration/standard/test_metrics.py"
228228
"tests/integration/standard/test_query.py"
229229
"tests/integration/simulacron/test_endpoint.py"
230+
"tests/integration/long/test_ssl.py"
230231
)
231232
EVENT_LOOP_MANAGER=$EVENT_LOOP_MANAGER CCM_ARGS="$CCM_ARGS" CASSANDRA_VERSION=$CCM_CASSANDRA_VERSION MAPPED_CASSANDRA_VERSION=$MAPPED_CASSANDRA_VERSION VERIFY_CYTHON=$FORCE_CYTHON nosetests -s -v --logging-format="[%(levelname)s] %(asctime)s %(thread)d: %(message)s" --with-ignore-docstrings --with-xunit --xunit-file=standard_results.xml ${EVENT_LOOP_TESTS[@]} || true
232233
exit 0

cassandra/cluster.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,6 @@
3737
import weakref
3838
from weakref import WeakValueDictionary
3939

40-
try:
41-
from cassandra.io.twistedreactor import TwistedConnection
42-
except ImportError:
43-
TwistedConnection = None
44-
45-
try:
46-
from weakref import WeakSet
47-
except ImportError:
48-
from cassandra.util import WeakSet # NOQA
49-
5040
from cassandra import (ConsistencyLevel, AuthenticationFailed,
5141
OperationTimedOut, UnsupportedOperation,
5242
SchemaTargetType, DriverException, ProtocolVersion,
@@ -85,6 +75,21 @@
8575
from cassandra.compat import Mapping
8676
from cassandra.datastax import cloud as dscloud
8777

78+
try:
79+
from cassandra.io.twistedreactor import TwistedConnection
80+
except ImportError:
81+
TwistedConnection = None
82+
83+
try:
84+
from cassandra.io.eventletreactor import EventletConnection
85+
except ImportError:
86+
EventletConnection = None
87+
88+
try:
89+
from weakref import WeakSet
90+
except ImportError:
91+
from cassandra.util import WeakSet # NOQA
92+
8893

8994
def _is_eventlet_monkey_patched():
9095
if 'eventlet.patcher' not in sys.modules:
@@ -920,10 +925,9 @@ def __init__(self,
920925
raise ValueError("contact_points, endpoint_factory, ssl_context, and ssl_options "
921926
"cannot be specified with a cloud configuration")
922927

923-
cloud_config = dscloud.get_cloud_config(
924-
cloud,
925-
create_pyopenssl_context=self.connection_class is TwistedConnection
926-
)
928+
uses_twisted = TwistedConnection and issubclass(self.connection_class, TwistedConnection)
929+
uses_eventlet = EventletConnection and issubclass(self.connection_class, EventletConnection)
930+
cloud_config = dscloud.get_cloud_config(cloud, create_pyopenssl_context=uses_twisted or uses_eventlet)
927931

928932
ssl_context = cloud_config.ssl_context
929933
ssl_options = {'check_hostname': True}

cassandra/connection.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,15 @@ def factory(cls, endpoint, timeout, *args, **kwargs):
615615
else:
616616
return conn
617617

618+
def _wrap_socket_from_context(self):
619+
self._socket = self.ssl_context.wrap_socket(self._socket, **(self.ssl_options or {}))
620+
621+
def _initiate_connection(self, sockaddr):
622+
self._socket.connect(sockaddr)
623+
624+
def _match_hostname(self):
625+
ssl.match_hostname(self._socket.getpeercert(), self.endpoint.address)
626+
618627
def _get_socket_addresses(self):
619628
address, port = self.endpoint.resolve()
620629

@@ -634,17 +643,16 @@ def _connect_socket(self):
634643
try:
635644
self._socket = self._socket_impl.socket(af, socktype, proto)
636645
if self.ssl_context:
637-
self._socket = self.ssl_context.wrap_socket(self._socket,
638-
**(self.ssl_options or {}))
646+
self._wrap_socket_from_context()
639647
elif self.ssl_options:
640648
if not self._ssl_impl:
641649
raise RuntimeError("This version of Python was not compiled with SSL support")
642650
self._socket = self._ssl_impl.wrap_socket(self._socket, **self.ssl_options)
643651
self._socket.settimeout(self.connect_timeout)
644-
self._socket.connect(sockaddr)
652+
self._initiate_connection(sockaddr)
645653
self._socket.settimeout(None)
646654
if self._check_hostname:
647-
ssl.match_hostname(self._socket.getpeercert(), self.endpoint.address)
655+
self._match_hostname()
648656
sockerr = None
649657
break
650658
except socket.error as err:

cassandra/datastax/cloud/__init__.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
import os
1616
import logging
1717
import json
18+
import sys
1819
import tempfile
1920
import shutil
21+
import six
2022
from six.moves.urllib.request import urlopen
2123

2224
_HAS_SSL = True
@@ -177,8 +179,12 @@ def _ssl_context_from_cert(ca_cert_location, cert_location, key_location):
177179
def _pyopenssl_context_from_cert(ca_cert_location, cert_location, key_location):
178180
try:
179181
from OpenSSL import SSL
180-
except ImportError:
181-
return None
182+
except ImportError as e:
183+
six.reraise(
184+
ImportError,
185+
ImportError("PyOpenSSL must be installed to connect to Apollo with the Eventlet or Twisted event loops"),
186+
sys.exc_info()[2]
187+
)
182188
ssl_context = SSL.Context(SSL.TLSv1_METHOD)
183189
ssl_context.set_verify(SSL.VERIFY_PEER, callback=lambda _1, _2, _3, _4, ok: ok)
184190
ssl_context.use_certificate_file(cert_location)

cassandra/io/eventletreactor.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
# Originally derived from MagnetoDB source:
1717
# https://github.com/stackforge/magnetodb/blob/2015.1.0b1/magnetodb/common/cassandra/io/eventletreactor.py
18-
1918
import eventlet
2019
from eventlet.green import socket
2120
from eventlet.queue import Queue
@@ -27,11 +26,25 @@
2726
from six.moves import xrange
2827

2928
from cassandra.connection import Connection, ConnectionShutdown, Timer, TimerManager
29+
try:
30+
from eventlet.green.OpenSSL import SSL
31+
_PYOPENSSL = True
32+
except ImportError as e:
33+
_PYOPENSSL = False
34+
no_pyopenssl_error = e
3035

3136

3237
log = logging.getLogger(__name__)
3338

3439

40+
def _check_pyopenssl():
41+
if not _PYOPENSSL:
42+
raise ImportError(
43+
"{}, pyOpenSSL must be installed to enable "
44+
"SSL support with the Eventlet event loop".format(str(no_pyopenssl_error))
45+
)
46+
47+
3548
class EventletConnection(Connection):
3649
"""
3750
An implementation of :class:`.Connection` that utilizes ``eventlet``.
@@ -81,6 +94,7 @@ def service_timeouts(cls):
8194

8295
def __init__(self, *args, **kwargs):
8396
Connection.__init__(self, *args, **kwargs)
97+
self.uses_legacy_ssl_options = self.ssl_options and not self.ssl_context
8498
self._write_queue = Queue()
8599

86100
self._connect_socket()
@@ -89,6 +103,31 @@ def __init__(self, *args, **kwargs):
89103
self._write_watcher = eventlet.spawn(lambda: self.handle_write())
90104
self._send_options_message()
91105

106+
def _wrap_socket_from_context(self):
107+
_check_pyopenssl()
108+
self._socket = SSL.Connection(self.ssl_context, self._socket)
109+
self._socket.set_connect_state()
110+
if self.ssl_options and 'server_hostname' in self.ssl_options:
111+
# This is necessary for SNI
112+
self._socket.set_tlsext_host_name(self.ssl_options['server_hostname'].encode('ascii'))
113+
114+
def _initiate_connection(self, sockaddr):
115+
if self.uses_legacy_ssl_options:
116+
super(EventletConnection, self)._initiate_connection(sockaddr)
117+
else:
118+
self._socket.connect(sockaddr)
119+
if self.ssl_context or self.ssl_options:
120+
self._socket.do_handshake()
121+
122+
def _match_hostname(self):
123+
if self.uses_legacy_ssl_options:
124+
super(EventletConnection, self)._match_hostname()
125+
else:
126+
cert_name = self._socket.get_peer_certificate().get_subject().commonName
127+
if cert_name != self.endpoint.address:
128+
raise Exception("Hostname verification failed! Certificate name '{}' "
129+
"doesn't endpoint '{}'".format(cert_name, self.endpoint.address))
130+
92131
def close(self):
93132
with self.lock:
94133
if self.is_closed:

docs/cloud.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,4 @@ Limitations
3434

3535
Event loops
3636
^^^^^^^^^^^
37-
Evenlet isn't supported yet. Eventlet still uses the old way to configure
38-
SSL (ssl_options), which is not compatible with the secure connect bundle provided by Apollo.
37+
Evenlet isn't yet supported for python 3.7+ due to an `issue in Eventlet <https://github.com/eventlet/eventlet/issues/526>`_.

docs/security.rst

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,10 @@ It might be also useful to learn about the different levels of identity verifica
7878

7979
* `Using SSL in DSE drivers <https://docs.datastax.com/en/dse/6.7/dse-dev/datastax_enterprise/appDevGuide/sslDrivers.html>`_
8080

81-
SSL with Twisted
82-
^^^^^^^^^^^^^^^^
83-
Twisted uses an alternative SSL implementation called pyOpenSSL, so if your `Cluster`'s connection class is
84-
:class:`~cassandra.io.twistedreactor.TwistedConnection`, you must pass a
81+
SSL with Twisted or Eventlet
82+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
83+
Twisted and Eventlet both use an alternative SSL implementation called pyOpenSSL, so if your `Cluster`'s connection class is
84+
:class:`~cassandra.io.twistedreactor.TwistedConnection` or :class:`~cassandra.io.eventletreactor.EventletConnection`, you must pass a
8585
`pyOpenSSL context <https://www.pyopenssl.org/en/stable/api/ssl.html#context-objects>`_ instead.
8686
An example is provided in these docs, and more details can be found in the
8787
`documentation <https://www.pyopenssl.org/en/stable/api/ssl.html#context-objects>`_.
@@ -270,6 +270,10 @@ for more details about ``SSLContext`` configuration.
270270
)
271271
session = cluster.connect()
272272
273+
274+
Connecting using Eventlet would look similar except instead of importing and using ``TwistedConnection``, you would
275+
import and use ``EventletConnection``, including the appropriate monkey-patching.
276+
273277
Versions 3.16.0 and lower
274278
^^^^^^^^^^^^^^^^^^^^^^^^^
275279

tests/integration/long/test_ssl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
DRIVER_CERTFILE = os.path.abspath("tests/integration/long/ssl/driver.pem")
4343
DRIVER_CERTFILE_BAD = os.path.abspath("tests/integration/long/ssl/python_driver_bad.pem")
4444

45-
USES_PYOPENSSL = "twisted" in EVENT_LOOP_MANAGER
45+
USES_PYOPENSSL = "twisted" in EVENT_LOOP_MANAGER or "eventlet" in EVENT_LOOP_MANAGER
4646
if "twisted" in EVENT_LOOP_MANAGER:
4747
import OpenSSL
4848
ssl_version = OpenSSL.SSL.TLSv1_METHOD

0 commit comments

Comments
 (0)