8
8
import time
9
9
import errno
10
10
import socket
11
+ try :
12
+ import ssl
13
+ is_ssl_supported = True
14
+ except ImportError :
15
+ is_ssl_supported = False
16
+ import sys
11
17
import abc
12
18
13
19
import ctypes
19
25
20
26
import msgpack
21
27
22
- import tarantool .error
23
28
from tarantool .response import Response
24
29
from tarantool .request import (
25
30
Request ,
44
49
SOCKET_TIMEOUT ,
45
50
RECONNECT_MAX_ATTEMPTS ,
46
51
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 ,
47
58
REQUEST_TYPE_OK ,
48
59
REQUEST_TYPE_ERROR ,
49
60
IPROTO_GREETING_SIZE ,
53
64
from tarantool .error import (
54
65
Error ,
55
66
NetworkError ,
67
+ SslError ,
56
68
DatabaseError ,
57
69
InterfaceError ,
58
70
ConfigurationError ,
@@ -196,15 +208,28 @@ def __init__(self, host, port,
196
208
encoding = ENCODING_DEFAULT ,
197
209
use_list = True ,
198
210
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 ):
200
217
'''
201
218
Initialize a connection to the server.
202
219
203
220
:param str host: Server hostname or IP-address
204
221
:param int port: Server port
205
222
: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.
208
233
'''
209
234
210
235
if msgpack .version >= (1 , 0 , 0 ) and encoding not in (None , 'utf-8' ):
@@ -237,6 +262,11 @@ def __init__(self, host, port,
237
262
self .use_list = use_list
238
263
self .call_16 = call_16
239
264
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
240
270
if connect_now :
241
271
self .connect ()
242
272
@@ -255,14 +285,15 @@ def is_closed(self):
255
285
return self ._socket is None
256
286
257
287
def connect_basic (self ):
258
- if self .host == None :
288
+ if self .host is None :
259
289
self .connect_unix ()
260
290
else :
261
291
self .connect_tcp ()
262
292
263
293
def connect_tcp (self ):
264
294
'''
265
295
Create connection to the host and port specified in __init__().
296
+
266
297
:raise: `NetworkError`
267
298
'''
268
299
@@ -282,6 +313,7 @@ def connect_tcp(self):
282
313
def connect_unix (self ):
283
314
'''
284
315
Create connection to the host and port specified in __init__().
316
+
285
317
:raise: `NetworkError`
286
318
'''
287
319
@@ -298,6 +330,73 @@ def connect_unix(self):
298
330
self .connected = False
299
331
raise NetworkError (e )
300
332
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
+ # If the password argument is not specified and a password is
370
+ # required, OpenSSL’s built-in password prompting mechanism
371
+ # will be used to interactively prompt the user for a password.
372
+ #
373
+ # We should disable this behaviour, because a python
374
+ # application that uses the connector unlikely assumes
375
+ # interaction with a human + a Tarantool implementation does
376
+ # not support this at least for now.
377
+ def password_raise_error ():
378
+ raise SslError ("a password for decrypting the private " +
379
+ "key is unsupported" )
380
+ context .load_cert_chain (certfile = self .ssl_cert_file ,
381
+ keyfile = self .ssl_key_file ,
382
+ password = password_raise_error )
383
+
384
+ if self .ssl_ca_file :
385
+ context .load_verify_locations (cafile = self .ssl_ca_file )
386
+ context .verify_mode = ssl .CERT_REQUIRED
387
+ # A Tarantool implementation does not check hostname. We don't
388
+ # do that too. As a result we don't set here:
389
+ # context.check_hostname = True
390
+
391
+ if self .ssl_ciphers :
392
+ context .set_ciphers (self .ssl_ciphers )
393
+
394
+ self ._socket = context .wrap_socket (self ._socket )
395
+ except SslError as e :
396
+ raise e
397
+ except Exception as e :
398
+ raise SslError (e )
399
+
301
400
def handshake (self ):
302
401
greeting_buf = self ._recv (IPROTO_GREETING_SIZE )
303
402
greeting = greeting_decode (greeting_buf )
@@ -316,11 +415,16 @@ def connect(self):
316
415
since it is called when you create an `Connection` instance.
317
416
318
417
:raise: `NetworkError`
418
+ :raise: `SslError`
319
419
'''
320
420
try :
321
421
self .connect_basic ()
422
+ if self .transport == SSL_TRANSPORT :
423
+ self .wrap_socket_ssl ()
322
424
self .handshake ()
323
425
self .load_schema ()
426
+ except SslError as e :
427
+ raise e
324
428
except Exception as e :
325
429
self .connected = False
326
430
raise NetworkError (e )
@@ -447,6 +551,8 @@ def check(): # Check that connection is alive
447
551
raise NetworkError (
448
552
socket .error (last_errno , errno .errorcode [last_errno ]))
449
553
attempt += 1
554
+ if self .transport == SSL_TRANSPORT :
555
+ self .wrap_socket_ssl ()
450
556
self .handshake ()
451
557
452
558
def _send_request (self , request ):
0 commit comments