Skip to content

Commit 1c8dfb1

Browse files
committed
DevAPI: Core TLS/SSL options for the mysqlx URI scheme
This patch adds the ability to connect to a MySQLx enabled server with SSL. New connection options added: 1. ssl-enable 2. ssl-ca 3. ssl-cert 4. ssl-key ssl-enable can be used by itself to use default SSL settings. Tests were added for regression.
1 parent c7a29ac commit 1c8dfb1

File tree

5 files changed

+116
-8
lines changed

5 files changed

+116
-8
lines changed

lib/mysqlx/__init__.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525

2626
import re
2727

28-
import constants
28+
from . import constants
2929

30-
from .compat import STRING_TYPES, urlparse
30+
from .compat import STRING_TYPES, urlparse, parse_qsl
3131
from .connection import XSession, NodeSession
3232
from .crud import Schema, Collection, Table, View
3333
from .dbdoc import DbDoc
@@ -108,12 +108,16 @@ def _parse_connection_uri(uri):
108108
if parsed.hostname is None or parsed.username is None \
109109
or parsed.password is None:
110110
raise InterfaceError("Malformed URI '{0}'".format(uri))
111+
111112
settings = {
112113
"user": parsed.username,
113114
"password": parsed.password,
114115
"schema": parsed.path.lstrip("/")
115116
}
116117

118+
query = dict(parse_qsl(parsed.query, True))
119+
for opt, val in query.items():
120+
settings[opt] = val.strip("() ") or True
117121
settings.update(_parse_address_list(parsed.netloc.split("@")[-1]))
118122
return settings
119123

@@ -172,6 +176,10 @@ def _get_connection_settings(*args, **kwargs):
172176
else:
173177
_validate_settings(settings)
174178

179+
ssl_opts = ["ssl-key", "ssl-cert", "ssl-ca", "ssl-crl"]
180+
if any(key in settings for key in ssl_opts):
181+
settings["ssl-enable"] = True
182+
175183
return settings
176184

177185
def get_session(*args, **kwargs):

lib/mysqlx/compat.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333

3434
if PY3:
35-
from urllib.parse import urlparse
35+
from urllib.parse import urlparse, parse_qsl
3636

3737
def hexlify(data):
3838
return binascii.hexlify(data).decode("utf-8")
@@ -45,7 +45,7 @@ def hexlify(data):
4545

4646

4747
else:
48-
from urlparse import urlparse
48+
from urlparse import urlparse, parse_qsl
4949

5050
def hexlify(data):
5151
return data.encode("hex")

lib/mysqlx/connection.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323

2424
"""Implementation of communication for MySQL X servers."""
2525

26+
try:
27+
import ssl
28+
SSL_AVAILABLE = True
29+
except:
30+
SSL_AVAILABLE = False
31+
2632
import socket
2733

2834
from functools import wraps
@@ -42,6 +48,7 @@
4248
class SocketStream(object):
4349
def __init__(self):
4450
self._socket = None
51+
self._is_ssl = False
4552

4653
def connect(self, host, port):
4754
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -68,6 +75,34 @@ def close(self):
6875
self._socket.close()
6976
self._socket = None
7077

78+
def set_ssl(self, ssl_opts={}):
79+
if not SSL_AVAILABLE:
80+
raise RuntimeError("Python installation has no SSL support.")
81+
82+
context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
83+
context.load_default_certs()
84+
if "ssl-ca" in ssl_opts:
85+
try:
86+
context.load_verify_locations(ssl_opts["ssl-ca"])
87+
context.verify_mode = ssl.CERT_REQUIRED
88+
except (IOError, SSLError):
89+
raise InterfaceError("Invalid CA certificate.")
90+
if "ssl-crl" in ssl_opts:
91+
try:
92+
context.load_verify_locations(ssl_opts["ssl-crl"])
93+
context.verify_flags = ssl.VERIFY_CRL_CHECK_CHAIN
94+
except (IOError, SSLError):
95+
raise InterfaceError("Invalid CRL.")
96+
if "ssl-cert" in ssl_opts:
97+
try:
98+
context.load_cert_chain(ssl_opts["ssl-cert"],
99+
ssl_opts.get("ssl-key", None))
100+
except (IOError, SSLError):
101+
raise InterfaceError("Invalid Client Certificate/Key.")
102+
103+
self._socket = context.wrap_socket(self._socket)
104+
self._is_ssl = True
105+
71106

72107
def catch_network_exception(func):
73108
@wraps(func)
@@ -104,10 +139,16 @@ def connect(self, host, port):
104139
self._authenticate()
105140

106141
def _handle_capabilities(self):
107-
# TODO: To implement
108-
# caps = mysqlx_connection_pb2.CapabilitiesGet()
109-
# data = caps.SerializeToString()
110-
pass
142+
if not self.settings.get("ssl-enable", False):
143+
return
144+
145+
data = self.protocol.get_capabilites()
146+
if not next((True for cap in data.capabilities \
147+
if cap.name == "tls"), False):
148+
raise OperationalError("SSL is not enabled on server.")
149+
150+
self.protocol.set_capabilities(tls=True)
151+
self.stream.set_ssl(self.settings)
111152

112153
def _authenticate(self):
113154
plugin = MySQL41AuthPlugin(self._user, self._password)

lib/mysqlx/protocol.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from .protobuf import mysqlx_resultset_pb2 as MySQLxResultset
3434
from .protobuf import mysqlx_crud_pb2 as MySQLxCrud
3535
from .protobuf import mysqlx_expr_pb2 as MySQLxExpr
36+
from .protobuf import mysqlx_connection_pb2 as MySQLxConnection
3637
from .result import ColumnMetaData
3738
from .compat import STRING_TYPES, INT_TYPES
3839
from .dbdoc import DbDoc
@@ -56,6 +57,7 @@
5657
(MySQLx.ServerMessages.RESULTSET_FETCH_DONE_MORE_RESULTSETS,
5758
MySQLxResultset.FetchDoneMoreResultsets),
5859
(MySQLx.ServerMessages.OK, MySQLx.Ok),
60+
(MySQLx.ServerMessages.CONN_CAPABILITIES, MySQLxConnection.Capabilities),
5961
]
6062

6163

@@ -105,6 +107,24 @@ def __init__(self, reader_writer):
105107
self._writer = reader_writer
106108
self._message = None
107109

110+
def get_capabilites(self):
111+
msg = MySQLxConnection.CapabilitiesGet()
112+
self._writer.write_message(
113+
MySQLx.ClientMessages.CON_CAPABILITIES_GET, msg)
114+
return self._reader.read_message()
115+
116+
def set_capabilities(self, **kwargs):
117+
msg = MySQLxConnection.CapabilitiesSet()
118+
for key, value in kwargs.items():
119+
value = self._create_any(value)
120+
capability = MySQLxConnection.Capability(name=key, value=value)
121+
msg.capabilities.capabilities.extend([capability])
122+
123+
self._writer.write_message(
124+
MySQLx.ClientMessages.CON_CAPABILITIES_SET, msg)
125+
126+
return self.read_ok()
127+
108128
def send_auth_start(self, method):
109129
msg = MySQLxSession.AuthenticateStart(mech_name=method)
110130
self._writer.write_message(

tests/test_mysqlx_connection.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,26 @@ def test_bind_to_default_shard(self):
273273
tests.MYSQL_SERVERS[0].start()
274274
tests.MYSQL_SERVERS[0].wait_up()
275275

276+
def test_ssl_connection(self):
277+
config = {}
278+
config.update(self.connect_kwargs)
279+
config["ssl-ca"] = tests.SSL_CA
280+
config["ssl-cert"] = tests.SSL_CERT
281+
config["ssl-key"] = tests.SSL_KEY
282+
283+
session = mysqlx.get_session(config)
284+
285+
res = mysqlx.statement.SqlStatement(session._connection,
286+
"SHOW STATUS LIKE 'Mysqlx_ssl_active'").execute().fetch_all()
287+
self.assertEqual("ON", res[0][1])
288+
289+
res = mysqlx.statement.SqlStatement(session._connection,
290+
"SHOW STATUS LIKE 'Mysqlx_ssl_version'").execute().fetch_all()
291+
self.assertTrue("TLS" in res[0][1])
292+
293+
session.close()
294+
295+
276296
@unittest.skipIf(tests.MYSQL_VERSION < (5, 7, 12), "XPlugin not compatible")
277297
class MySQLxNodeSessionTests(tests.MySQLxTests):
278298

@@ -384,3 +404,22 @@ def test_commit(self):
384404
self.assertEqual(table.count(), 1)
385405

386406
schema.drop_table(table_name)
407+
408+
def test_ssl_connection(self):
409+
config = {}
410+
config.update(self.connect_kwargs)
411+
config["ssl-ca"] = tests.SSL_CA
412+
config["ssl-cert"] = tests.SSL_CERT
413+
config["ssl-key"] = tests.SSL_KEY
414+
415+
session = mysqlx.get_node_session(config)
416+
417+
res = session.sql("SHOW STATUS LIKE 'Mysqlx_ssl_active'") \
418+
.execute().fetch_all()
419+
self.assertEqual("ON", res[0][1])
420+
421+
res = session.sql("SHOW STATUS LIKE 'Mysqlx_ssl_version'") \
422+
.execute().fetch_all()
423+
self.assertTrue("TLS" in res[0][1])
424+
425+
session.close()

0 commit comments

Comments
 (0)