88import time
99import errno
1010import socket
11+ try :
12+ import ssl
13+ is_ssl_supported = True
14+ except ImportError :
15+ is_ssl_supported = False
16+ import sys
1117import abc
1218
1319import ctypes
1925
2026import msgpack
2127
22- import tarantool .error
2328from tarantool .response import Response
2429from tarantool .request import (
2530 Request ,
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 ,
5364from 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,73 @@ 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+ # 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+
301400 def handshake (self ):
302401 greeting_buf = self ._recv (IPROTO_GREETING_SIZE )
303402 greeting = greeting_decode (greeting_buf )
@@ -316,11 +415,16 @@ def connect(self):
316415 since it is called when you create an `Connection` instance.
317416
318417 :raise: `NetworkError`
418+ :raise: `SslError`
319419 '''
320420 try :
321421 self .connect_basic ()
422+ if self .transport == SSL_TRANSPORT :
423+ self .wrap_socket_ssl ()
322424 self .handshake ()
323425 self .load_schema ()
426+ except SslError as e :
427+ raise e
324428 except Exception as e :
325429 self .connected = False
326430 raise NetworkError (e )
@@ -447,6 +551,8 @@ def check(): # Check that connection is alive
447551 raise NetworkError (
448552 socket .error (last_errno , errno .errorcode [last_errno ]))
449553 attempt += 1
554+ if self .transport == SSL_TRANSPORT :
555+ self .wrap_socket_ssl ()
450556 self .handshake ()
451557
452558 def _send_request (self , request ):
0 commit comments