Skip to content

Commit ff7e388

Browse files
committed
api: support of SSL protocol
The patch adds support for using SSL to encrypt the client-server communications [1]. 1. https://www.tarantool.io/en/enterprise_doc/security/#enterprise-iproto-encryption Part of #217
1 parent 46907c7 commit ff7e388

15 files changed

+708
-50
lines changed

Diff for: CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## Unreleased
88

99
### Added
10+
- SSL support (PR #220, #217).
1011

1112
### Changed
1213

Diff for: Makefile

+2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
.PHONY: test
22
test:
33
python setup.py test
4+
testdata:
5+
cd ./test/data/; ./generate.sh
46
coverage:
57
python -m coverage run -p --source=. setup.py test
68
cov-html:

Diff for: tarantool/connection.py

+78-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
import time
99
import errno
1010
import socket
11+
try:
12+
import ssl
13+
is_ssl_supported = True
14+
except ImportError:
15+
is_ssl_supported = False
1116
import abc
1217

1318
import ctypes
@@ -44,6 +49,12 @@
4449
SOCKET_TIMEOUT,
4550
RECONNECT_MAX_ATTEMPTS,
4651
RECONNECT_DELAY,
52+
DEFAULT_TRANSPORT,
53+
SSL_TRANSPORT,
54+
DEFAULT_SSL_KEY_FILE,
55+
DEFAULT_SSL_CERT_FILE,
56+
DEFAULT_SSL_CA_FILE,
57+
DEFAULT_SSL_CIPHERS,
4758
REQUEST_TYPE_OK,
4859
REQUEST_TYPE_ERROR,
4960
IPROTO_GREETING_SIZE,
@@ -196,15 +207,28 @@ def __init__(self, host, port,
196207
encoding=ENCODING_DEFAULT,
197208
use_list=True,
198209
call_16=False,
199-
connection_timeout=CONNECTION_TIMEOUT):
210+
connection_timeout=CONNECTION_TIMEOUT,
211+
transport=DEFAULT_TRANSPORT,
212+
ssl_key_file=DEFAULT_SSL_KEY_FILE,
213+
ssl_cert_file=DEFAULT_SSL_CERT_FILE,
214+
ssl_ca_file=DEFAULT_SSL_CA_FILE,
215+
ssl_ciphers=DEFAULT_SSL_CIPHERS):
200216
'''
201217
Initialize a connection to the server.
202218
203219
:param str host: Server hostname or IP-address
204220
:param int port: Server port
205221
:param bool connect_now: if True (default) than __init__() actually
206-
creates network connection.
207-
if False than you have to call connect() manualy.
222+
creates network connection. if False than you have to call connect()
223+
manualy.
224+
:param str transport: It enables SSL encryption for a connection if set
225+
to ssl.
226+
:param str ssl_key_file: A path to a private SSL key file.
227+
:param str ssl_cert_file: A path to an SSL certificate file.
228+
:param str ssl_ca_file: A path to a trusted certificate authorities (CA)
229+
file.
230+
:param str ssl_ciphers: A colon-separated (:) list of SSL cipher suites
231+
the connection can use.
208232
'''
209233

210234
if msgpack.version >= (1, 0, 0) and encoding not in (None, 'utf-8'):
@@ -237,6 +261,11 @@ def __init__(self, host, port,
237261
self.use_list = use_list
238262
self.call_16 = call_16
239263
self.connection_timeout = connection_timeout
264+
self.transport = transport
265+
self.ssl_key_file = ssl_key_file
266+
self.ssl_cert_file = ssl_cert_file
267+
self.ssl_ca_file = ssl_ca_file
268+
self.ssl_ciphers = ssl_ciphers
240269
if connect_now:
241270
self.connect()
242271

@@ -263,6 +292,7 @@ def connect_basic(self):
263292
def connect_tcp(self):
264293
'''
265294
Create connection to the host and port specified in __init__().
295+
266296
:raise: `NetworkError`
267297
'''
268298

@@ -282,6 +312,7 @@ def connect_tcp(self):
282312
def connect_unix(self):
283313
'''
284314
Create connection to the host and port specified in __init__().
315+
285316
:raise: `NetworkError`
286317
'''
287318

@@ -298,6 +329,46 @@ def connect_unix(self):
298329
self.connected = False
299330
raise NetworkError(e)
300331

332+
def wrap_socket_ssl(self):
333+
'''
334+
Wrap an existing socket with SSL socket.
335+
336+
:raise: NetworkError
337+
:raise: `ssl.SSLError`
338+
'''
339+
if is_ssl_supported == False:
340+
raise NetworkError("SSL is unsupported by the python.")
341+
342+
if ssl.TLSVersion:
343+
# Since python 3.7
344+
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
345+
# Reset to default OpenSSL values
346+
context.check_hostname = False
347+
context.verify_mode = ssl.CERT_NONE
348+
# Require TLSv1.2, because other protocol versions don't seem to
349+
# support the GOST cipher.
350+
context.minimum_version = ssl.TLSVersion.TLSv1_2
351+
context.maximum_version = ssl.TLSVersion.TLSv1_2
352+
else:
353+
# Deprecated, but it works for python < 3.7
354+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
355+
356+
if self.ssl_cert_file:
357+
context.load_cert_chain(certfile = self.ssl_cert_file,
358+
keyfile = self.ssl_key_file)
359+
360+
if self.ssl_ca_file:
361+
context.load_verify_locations(cafile=self.ssl_ca_file)
362+
context.verify_mode = ssl.CERT_REQUIRED
363+
if self.host:
364+
context.check_hostname = True
365+
366+
if self.ssl_ciphers:
367+
context.set_ciphers(self.ssl_ciphers)
368+
369+
self._socket = context.wrap_socket(self._socket,
370+
server_hostname = self.host)
371+
301372
def handshake(self):
302373
greeting_buf = self._recv(IPROTO_GREETING_SIZE)
303374
greeting = greeting_decode(greeting_buf)
@@ -319,6 +390,8 @@ def connect(self):
319390
'''
320391
try:
321392
self.connect_basic()
393+
if self.transport == SSL_TRANSPORT:
394+
self.wrap_socket_ssl()
322395
self.handshake()
323396
self.load_schema()
324397
except Exception as e:
@@ -447,6 +520,8 @@ def check(): # Check that connection is alive
447520
raise NetworkError(
448521
socket.error(last_errno, errno.errorcode[last_errno]))
449522
attempt += 1
523+
if self.transport == SSL_TRANSPORT:
524+
self.wrap_socket_ssl()
450525
self.handshake()
451526

452527
def _send_request(self, request):

Diff for: tarantool/connection_pool.py

+40-24
Original file line numberDiff line numberDiff line change
@@ -190,13 +190,13 @@ class ConnectionPool(ConnectionInterface):
190190
ConnectionPool API is the same as a plain Connection API.
191191
On each request, a connection is chosen to execute this request.
192192
Connection is selected based on request mode:
193+
193194
* Mode.ANY chooses any instance.
194195
* Mode.RW chooses an RW instance.
195196
* Mode.RO chooses an RO instance.
196-
* Mode.PREFER_RW chooses an RW instance, if possible, RO instance
197-
otherwise.
198-
* Mode.PREFER_RO chooses an RO instance, if possible, RW instance
199-
otherwise.
197+
* Mode.PREFER_RW chooses an RW instance, if possible, RO instance otherwise.
198+
* Mode.PREFER_RO chooses an RO instance, if possible, RW instance otherwise.
199+
200200
All requests that are guaranteed to write (insert, replace, delete,
201201
upsert, update) use RW mode by default. select uses ANY by default. You
202202
can set the mode explicitly. call, eval, execute and ping requests
@@ -218,28 +218,39 @@ def __init__(self,
218218
'''
219219
Initialize connections to the cluster of servers.
220220
221-
:param list addrs: List of {host: , port:} dictionaries,
222-
describing server addresses.
223-
:user str Username used to authenticate. User must be able
224-
to call box.info function. For example, to give grants to
225-
'guest' user, evaluate
226-
box.schema.func.create('box.info')
227-
box.schema.user.grant('guest', 'execute', 'function', 'box.info')
228-
on Tarantool instances.
221+
:param list addrs: List of
222+
223+
.. code-block:: python
224+
225+
{ host: "str",
226+
port: int,
227+
transport: "str",
228+
ssl_key_file: "str",
229+
ssl_cert_file: "str",
230+
ssl_ca_file: "str",
231+
ssl_ciphers: "str" }
232+
233+
dictionaries, describing server addresses.
234+
:param str user: Username used to authenticate. User must be able
235+
to call box.info function. For example, to give grants to
236+
'guest' user, evaluate
237+
box.schema.func.create('box.info')
238+
box.schema.user.grant('guest', 'execute', 'function', 'box.info')
239+
on Tarantool instances.
229240
:param int reconnect_max_attempts: Max attempts to reconnect
230-
for each connection in the pool. Be careful with reconnect
231-
parameters in ConnectionPool since every status refresh is
232-
also a request with reconnection. Default is 0 (fail after
233-
first attempt).
241+
for each connection in the pool. Be careful with reconnect
242+
parameters in ConnectionPool since every status refresh is
243+
also a request with reconnection. Default is 0 (fail after
244+
first attempt).
234245
:param float reconnect_delay: Time between reconnect
235-
attempts for each connection in the pool. Be careful with
236-
reconnect parameters in ConnectionPool since every status
237-
refresh is also a request with reconnection. Default is 0.
246+
attempts for each connection in the pool. Be careful with
247+
reconnect parameters in ConnectionPool since every status
248+
refresh is also a request with reconnection. Default is 0.
238249
:param StrategyInterface strategy_class: Class for choosing
239-
instance based on request mode. By default, round-robin
240-
strategy is used.
250+
instance based on request mode. By default, round-robin
251+
strategy is used.
241252
:param int refresh_delay: Minimal time between RW/RO status
242-
refreshes.
253+
refreshes.
243254
'''
244255

245256
if not isinstance(addrs, list) or len(addrs) == 0:
@@ -272,7 +283,12 @@ def __init__(self,
272283
connect_now=False, # Connect in ConnectionPool.connect()
273284
encoding=encoding,
274285
call_16=call_16,
275-
connection_timeout=connection_timeout)
286+
connection_timeout=connection_timeout,
287+
transport=addr['transport'],
288+
ssl_key_file=addr['ssl_key_file'],
289+
ssl_cert_file=addr['ssl_cert_file'],
290+
ssl_ca_file=addr['ssl_ca_file'],
291+
ssl_ciphers=addr['ssl_ciphers'])
276292
)
277293

278294
if connect_now:
@@ -464,7 +480,7 @@ def ping(self, *, mode=None, **kwargs):
464480
def select(self, space_name, key, *, mode=Mode.ANY, **kwargs):
465481
'''
466482
:param tarantool.Mode mode: Request mode (default is
467-
ANY).
483+
ANY).
468484
'''
469485

470486
return self._send(mode, 'select', space_name, key, **kwargs)

Diff for: tarantool/const.py

+12
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@
9494
RECONNECT_MAX_ATTEMPTS = 10
9595
# Default delay between attempts to reconnect (seconds)
9696
RECONNECT_DELAY = 0.1
97+
# Default value for transport
98+
DEFAULT_TRANSPORT = ""
99+
# Value for SSL transport
100+
SSL_TRANSPORT = "ssl"
101+
# Default value for a path to SSL key file
102+
DEFAULT_SSL_KEY_FILE = None
103+
# Default value for a path to SSL certificate file
104+
DEFAULT_SSL_CERT_FILE = None
105+
# Default value for a path to SSL certificatea uthorities file
106+
DEFAULT_SSL_CA_FILE = None
107+
# Default value for list of SSL ciphers
108+
DEFAULT_SSL_CIPHERS = None
97109
# Default cluster nodes list refresh interval (seconds)
98110
CLUSTER_DISCOVERY_DELAY = 60
99111
# Default cluster nodes state refresh interval (seconds)

Diff for: tarantool/error.py

+8
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121

2222
import os
2323
import socket
24+
try:
25+
import ssl
26+
is_ssl_supported = True
27+
except ImportError:
28+
is_ssl_supported = False
2429
import sys
2530
import warnings
2631

@@ -205,10 +210,13 @@ def __init__(self, orig_exception=None, *args):
205210
if isinstance(orig_exception, socket.timeout):
206211
self.message = "Socket timeout"
207212
super(NetworkError, self).__init__(0, self.message)
213+
elif is_ssl_supported and isinstance(orig_exception, ssl.SSLError):
214+
super(NetworkError, self).__init__(orig_exception, *args)
208215
elif isinstance(orig_exception, socket.error):
209216
self.message = os.strerror(orig_exception.errno)
210217
super(NetworkError, self).__init__(
211218
orig_exception.errno, self.message)
219+
212220
else:
213221
super(NetworkError, self).__init__(orig_exception, *args)
214222

0 commit comments

Comments
 (0)