Skip to content

Commit 58dbfdc

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 58dbfdc

17 files changed

+857
-88
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

+103-5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
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
16+
import sys
1117
import abc
1218

1319
import ctypes
@@ -19,7 +25,6 @@
1925

2026
import msgpack
2127

22-
import tarantool.error
2328
from tarantool.response import Response
2429
from tarantool.request import (
2530
Request,
@@ -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,
@@ -53,6 +64,7 @@
5364
from tarantool.error import (
5465
Error,
5566
NetworkError,
67+
SslError,
5668
DatabaseError,
5769
InterfaceError,
5870
ConfigurationError,
@@ -196,15 +208,28 @@ def __init__(self, host, port,
196208
encoding=ENCODING_DEFAULT,
197209
use_list=True,
198210
call_16=False,
199-
connection_timeout=CONNECTION_TIMEOUT):
211+
connection_timeout=CONNECTION_TIMEOUT,
212+
transport=DEFAULT_TRANSPORT,
213+
ssl_key_file=DEFAULT_SSL_KEY_FILE,
214+
ssl_cert_file=DEFAULT_SSL_CERT_FILE,
215+
ssl_ca_file=DEFAULT_SSL_CA_FILE,
216+
ssl_ciphers=DEFAULT_SSL_CIPHERS):
200217
'''
201218
Initialize a connection to the server.
202219
203220
:param str host: Server hostname or IP-address
204221
:param int port: Server port
205222
:param bool connect_now: if True (default) than __init__() actually
206-
creates network connection.
207-
if False than you have to call connect() manualy.
223+
creates network connection. if False than you have to call
224+
connect() manualy.
225+
:param str transport: It enables SSL encryption for a connection if set
226+
to ssl. At least Python 3.5 is required for SSL encryption.
227+
:param str ssl_key_file: A path to a private SSL key file.
228+
:param str ssl_cert_file: A path to an SSL certificate file.
229+
:param str ssl_ca_file: A path to a trusted certificate authorities
230+
(CA) file.
231+
:param str ssl_ciphers: A colon-separated (:) list of SSL cipher suites
232+
the connection can use.
208233
'''
209234

210235
if msgpack.version >= (1, 0, 0) and encoding not in (None, 'utf-8'):
@@ -237,6 +262,11 @@ def __init__(self, host, port,
237262
self.use_list = use_list
238263
self.call_16 = call_16
239264
self.connection_timeout = connection_timeout
265+
self.transport = transport
266+
self.ssl_key_file = ssl_key_file
267+
self.ssl_cert_file = ssl_cert_file
268+
self.ssl_ca_file = ssl_ca_file
269+
self.ssl_ciphers = ssl_ciphers
240270
if connect_now:
241271
self.connect()
242272

@@ -255,14 +285,15 @@ def is_closed(self):
255285
return self._socket is None
256286

257287
def connect_basic(self):
258-
if self.host == None:
288+
if self.host is None:
259289
self.connect_unix()
260290
else:
261291
self.connect_tcp()
262292

263293
def connect_tcp(self):
264294
'''
265295
Create connection to the host and port specified in __init__().
296+
266297
:raise: `NetworkError`
267298
'''
268299

@@ -282,6 +313,7 @@ def connect_tcp(self):
282313
def connect_unix(self):
283314
'''
284315
Create connection to the host and port specified in __init__().
316+
285317
:raise: `NetworkError`
286318
'''
287319

@@ -298,6 +330,65 @@ def connect_unix(self):
298330
self.connected = False
299331
raise NetworkError(e)
300332

333+
def wrap_socket_ssl(self):
334+
'''
335+
Wrap an existing socket with SSL socket.
336+
337+
:raise: SslError
338+
:raise: `ssl.SSLError`
339+
'''
340+
if not is_ssl_supported:
341+
raise SslError("SSL is unsupported by the python.")
342+
343+
ver = sys.version_info
344+
if ver[0] < 3 or (ver[0] == 3 and ver[1] < 5):
345+
raise SslError("SSL transport is supported only since " +
346+
"python 3.5")
347+
348+
if ((self.ssl_cert_file is None and self.ssl_key_file is not None)
349+
or (self.ssl_cert_file is not None and self.ssl_key_file is None)):
350+
raise SslError("ssl_cert_file and ssl_key_file should be both " +
351+
"configured or not")
352+
353+
try:
354+
if hasattr(ssl, 'TLSVersion'):
355+
# Since python 3.7
356+
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
357+
# Reset to default OpenSSL values.
358+
context.check_hostname = False
359+
context.verify_mode = ssl.CERT_NONE
360+
# Require TLSv1.2, because other protocol versions don't seem
361+
# to support the GOST cipher.
362+
context.minimum_version = ssl.TLSVersion.TLSv1_2
363+
context.maximum_version = ssl.TLSVersion.TLSv1_2
364+
else:
365+
# Deprecated, but it works for python < 3.7
366+
context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
367+
368+
if self.ssl_cert_file:
369+
def password_raise_error():
370+
raise SslError("a password for decrypting the private " +
371+
"key is unsupported")
372+
context.load_cert_chain(certfile=self.ssl_cert_file,
373+
keyfile=self.ssl_key_file,
374+
password=password_raise_error)
375+
376+
if self.ssl_ca_file:
377+
context.load_verify_locations(cafile=self.ssl_ca_file)
378+
context.verify_mode = ssl.CERT_REQUIRED
379+
if self.host:
380+
context.check_hostname = True
381+
382+
if self.ssl_ciphers:
383+
context.set_ciphers(self.ssl_ciphers)
384+
385+
self._socket = context.wrap_socket(self._socket,
386+
server_hostname=self.host)
387+
except SslError as e:
388+
raise e
389+
except Exception as e:
390+
raise SslError(e)
391+
301392
def handshake(self):
302393
greeting_buf = self._recv(IPROTO_GREETING_SIZE)
303394
greeting = greeting_decode(greeting_buf)
@@ -316,11 +407,16 @@ def connect(self):
316407
since it is called when you create an `Connection` instance.
317408
318409
:raise: `NetworkError`
410+
:raise: `SslError`
319411
'''
320412
try:
321413
self.connect_basic()
414+
if self.transport == SSL_TRANSPORT:
415+
self.wrap_socket_ssl()
322416
self.handshake()
323417
self.load_schema()
418+
except SslError as e:
419+
raise e
324420
except Exception as e:
325421
self.connected = False
326422
raise NetworkError(e)
@@ -447,6 +543,8 @@ def check(): # Check that connection is alive
447543
raise NetworkError(
448544
socket.error(last_errno, errno.errorcode[last_errno]))
449545
attempt += 1
546+
if self.transport == SSL_TRANSPORT:
547+
self.wrap_socket_ssl()
450548
self.handshake()
451549

452550
def _send_request(self, request):

Diff for: tarantool/connection_pool.py

+45-28
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,11 @@
2222
PoolTolopogyError,
2323
PoolTolopogyWarning,
2424
ConfigurationError,
25-
DatabaseError,
2625
NetworkError,
27-
NetworkWarning,
28-
tnt_strerror,
2926
warn
3027
)
3128
from tarantool.utils import ENCODING_DEFAULT
32-
from tarantool.mesh_connection import validate_address
29+
from tarantool.mesh_connection import prepare_address
3330

3431

3532
class Mode(Enum):
@@ -190,13 +187,15 @@ class ConnectionPool(ConnectionInterface):
190187
ConnectionPool API is the same as a plain Connection API.
191188
On each request, a connection is chosen to execute this request.
192189
Connection is selected based on request mode:
190+
193191
* Mode.ANY chooses any instance.
194192
* Mode.RW chooses an RW instance.
195193
* Mode.RO chooses an RO instance.
196194
* Mode.PREFER_RW chooses an RW instance, if possible, RO instance
197195
otherwise.
198196
* Mode.PREFER_RO chooses an RO instance, if possible, RW instance
199197
otherwise.
198+
200199
All requests that are guaranteed to write (insert, replace, delete,
201200
upsert, update) use RW mode by default. select uses ANY by default. You
202201
can set the mode explicitly. call, eval, execute and ping requests
@@ -218,39 +217,52 @@ def __init__(self,
218217
'''
219218
Initialize connections to the cluster of servers.
220219
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.
220+
:param list addrs: List of
221+
222+
.. code-block:: python
223+
224+
{ host: "str",
225+
port: int,
226+
transport: "str",
227+
ssl_key_file: "str",
228+
ssl_cert_file: "str",
229+
ssl_ca_file: "str",
230+
ssl_ciphers: "str" }
231+
232+
dictionaries, describing server addresses.
233+
:param str user: Username used to authenticate. User must be able
234+
to call box.info function. For example, to give grants to
235+
'guest' user, evaluate
236+
box.schema.func.create('box.info')
237+
box.schema.user.grant('guest', 'execute', 'function', 'box.info')
238+
on Tarantool instances.
229239
: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).
240+
for each connection in the pool. Be careful with reconnect
241+
parameters in ConnectionPool since every status refresh is
242+
also a request with reconnection. Default is 0 (fail after
243+
first attempt).
234244
: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.
245+
attempts for each connection in the pool. Be careful with
246+
reconnect parameters in ConnectionPool since every status
247+
refresh is also a request with reconnection. Default is 0.
238248
:param StrategyInterface strategy_class: Class for choosing
239-
instance based on request mode. By default, round-robin
240-
strategy is used.
249+
instance based on request mode. By default, round-robin
250+
strategy is used.
241251
:param int refresh_delay: Minimal time between RW/RO status
242-
refreshes.
252+
refreshes.
243253
'''
244254

245255
if not isinstance(addrs, list) or len(addrs) == 0:
246256
raise ConfigurationError("addrs must be non-empty list")
247257

248-
# Verify addresses.
258+
# Prepare addresses for usage.
259+
new_addrs = []
249260
for addr in addrs:
250-
ok, msg = validate_address(addr)
251-
if not ok:
261+
new_addr, msg = prepare_address(addr)
262+
if not new_addr:
252263
raise ConfigurationError(msg)
253-
self.addrs = addrs
264+
new_addrs.append(new_addr)
265+
self.addrs = new_addrs
254266

255267
# Create connections
256268
self.pool = {}
@@ -272,7 +284,12 @@ def __init__(self,
272284
connect_now=False, # Connect in ConnectionPool.connect()
273285
encoding=encoding,
274286
call_16=call_16,
275-
connection_timeout=connection_timeout)
287+
connection_timeout=connection_timeout,
288+
transport=addr['transport'],
289+
ssl_key_file=addr['ssl_key_file'],
290+
ssl_cert_file=addr['ssl_cert_file'],
291+
ssl_ca_file=addr['ssl_ca_file'],
292+
ssl_ciphers=addr['ssl_ciphers'])
276293
)
277294

278295
if connect_now:
@@ -464,7 +481,7 @@ def ping(self, *, mode=None, **kwargs):
464481
def select(self, space_name, key, *, mode=Mode.ANY, **kwargs):
465482
'''
466483
:param tarantool.Mode mode: Request mode (default is
467-
ANY).
484+
ANY).
468485
'''
469486

470487
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)

0 commit comments

Comments
 (0)