THRIFT-3505 Enhance Python TSSLSocket
This closes #760
diff --git a/lib/py/CMakeLists.txt b/lib/py/CMakeLists.txt
index ef987b6..7bb91fe 100755
--- a/lib/py/CMakeLists.txt
+++ b/lib/py/CMakeLists.txt
@@ -24,3 +24,8 @@
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Building Python library"
)
+
+if(BUILD_TESTING)
+ add_test(PythonTestSSLSocket ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/test_sslsocket.py)
+ add_test(PythonThriftJson ${PYTHON_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/thrift_json.py)
+endif()
diff --git a/lib/py/Makefile.am b/lib/py/Makefile.am
index c0db871..5183b9c 100755
--- a/lib/py/Makefile.am
+++ b/lib/py/Makefile.am
@@ -23,7 +23,8 @@
py3-build:
$(PYTHON3) setup.py build
py3-test: py3-build
- PYTHONPATH=build/lib $(PYTHON3) test/thrift_json.py
+ $(PYTHON3) test/thrift_json.py
+ $(PYTHON3) test/test_sslsocket.py
else
py3-build:
py3-test:
@@ -43,7 +44,8 @@
$(RM) -r build
check-local: all py3-test
- PYTHONPATH=$(shell ls build/lib* -dr | head -n1) $(PYTHON) test/thrift_json.py
+ $(PYTHON) test/thrift_json.py
+ $(PYTHON) test/test_sslsocket.py
EXTRA_DIST = \
CMakeLists.txt \
diff --git a/lib/py/src/transport/TSSLSocket.py b/lib/py/src/transport/TSSLSocket.py
index 6ad8d50..9be0912 100644
--- a/lib/py/src/transport/TSSLSocket.py
+++ b/lib/py/src/transport/TSSLSocket.py
@@ -17,73 +17,221 @@
# under the License.
#
+import logging
import os
import socket
import ssl
+import sys
+import warnings
from thrift.transport import TSocket
from thrift.transport.TTransport import TTransportException
+logger = logging.getLogger(__name__)
+warnings.filterwarnings('default', category=DeprecationWarning, module=__name__)
-class TSSLSocket(TSocket.TSocket):
+
+class TSSLBase(object):
+ # SSLContext is not available for Python < 2.7.9
+ _has_ssl_context = sys.hexversion >= 0x020709F0
+
+ # ciphers argument is not available for Python < 2.7.0
+ _has_ciphers = sys.hexversion >= 0x020700F0
+
+ # For pythoon >= 2.7.9, use latest TLS that both client and server supports.
+ # SSL 2.0 and 3.0 are disabled via ssl.OP_NO_SSLv2 and ssl.OP_NO_SSLv3.
+ # For pythoon < 2.7.9, use TLS 1.0 since TLSv1_X nare OP_NO_SSLvX are unavailable.
+ _default_protocol = ssl.PROTOCOL_SSLv23 if _has_ssl_context else ssl.PROTOCOL_TLSv1
+
+ def _init_context(self, ssl_version):
+ if self._has_ssl_context:
+ self._context = ssl.SSLContext(ssl_version)
+ if self._context.protocol == ssl.PROTOCOL_SSLv23:
+ self._context.options |= ssl.OP_NO_SSLv2
+ self._context.options |= ssl.OP_NO_SSLv3
+ else:
+ self._context = None
+ self._ssl_version = ssl_version
+
+ @property
+ def ssl_version(self):
+ if self._has_ssl_context:
+ return self.ssl_context.protocol
+ else:
+ return self._ssl_version
+
+ @property
+ def ssl_context(self):
+ return self._context
+
+ SSL_VERSION = _default_protocol
"""
- SSL implementation of client-side TSocket
+ Default SSL version.
+ For backword compatibility, it can be modified.
+ Use __init__ keywoard argument "ssl_version" instead.
+ """
+
+ def _deprecated_arg(self, args, kwargs, pos, key):
+ if len(args) <= pos:
+ return
+ real_pos = pos + 3
+ warnings.warn(
+ '%dth positional argument is deprecated. Use keyward argument insteand.' % real_pos,
+ DeprecationWarning)
+ if key in kwargs:
+ raise TypeError('Duplicate argument: %dth argument and %s keyward argument.', (real_pos, key))
+ kwargs[key] = args[pos]
+
+ def _unix_socket_arg(self, host, port, args, kwargs):
+ key = 'unix_socket'
+ if host is None and port is None and len(args) == 1 and key not in kwargs:
+ kwargs[key] = args[0]
+ return True
+ return False
+
+ def __getattr__(self, key):
+ if key == 'SSL_VERSION':
+ warnings.warn('Use ssl_version attribute instead.', DeprecationWarning)
+ return self.ssl_version
+
+ def __init__(self, server_side, host, ssl_opts):
+ self._server_side = server_side
+ if TSSLBase.SSL_VERSION != self._default_protocol:
+ warnings.warn('SSL_VERSION is deprecated. Use ssl_version keyward argument instead.', DeprecationWarning)
+ self._context = ssl_opts.pop('ssl_context', None)
+ self._server_hostname = None
+ if not self._server_side:
+ self._server_hostname = ssl_opts.pop('server_hostname', host)
+ if self._context:
+ self._custom_context = True
+ if ssl_opts:
+ raise ValueError('Incompatible arguments: ssl_context and %s' % ' '.join(ssl_opts.keys()))
+ if not self._has_ssl_context:
+ raise ValueError('ssl_context is not available for this version of Python')
+ else:
+ self._custom_context = False
+ ssl_version = ssl_opts.pop('ssl_version', TSSLBase.SSL_VERSION)
+ self._init_context(ssl_version)
+ self.cert_reqs = ssl_opts.pop('cert_reqs', ssl.CERT_REQUIRED)
+ self.ca_certs = ssl_opts.pop('ca_certs', None)
+ self.keyfile = ssl_opts.pop('keyfile', None)
+ self.certfile = ssl_opts.pop('certfile', None)
+ self.ciphers = ssl_opts.pop('ciphers', None)
+
+ if ssl_opts:
+ raise ValueError('Unknown keyword arguments: ', ' '.join(ssl_opts.keys()))
+
+ if self.cert_reqs != ssl.CERT_NONE:
+ if not self.ca_certs:
+ raise ValueError('ca_certs is needed when cert_reqs is not ssl.CERT_NONE')
+ if not os.access(self.ca_certs, os.R_OK):
+ raise IOError('Certificate Authority ca_certs file "%s" '
+ 'is not readable, cannot validate SSL '
+ 'certificates.' % (self.ca_certs))
+
+ @property
+ def certfile(self):
+ return self._certfile
+
+ @certfile.setter
+ def certfile(self, certfile):
+ if self._server_side and not certfile:
+ raise ValueError('certfile is needed for server-side')
+ if certfile and not os.access(certfile, os.R_OK):
+ raise IOError('No such certfile found: %s' % (certfile))
+ self._certfile = certfile
+
+ def _wrap_socket(self, sock):
+ if self._has_ssl_context:
+ if not self._custom_context:
+ self.ssl_context.verify_mode = self.cert_reqs
+ if self.certfile:
+ self.ssl_context.load_cert_chain(self.certfile, self.keyfile)
+ if self.ciphers:
+ self.ssl_context.set_ciphers(self.ciphers)
+ if self.ca_certs:
+ self.ssl_context.load_verify_locations(self.ca_certs)
+ return self.ssl_context.wrap_socket(sock, server_side=self._server_side,
+ server_hostname=self._server_hostname)
+ else:
+ ssl_opts = {
+ 'ssl_version': self._ssl_version,
+ 'server_side': self._server_side,
+ 'ca_certs': self.ca_certs,
+ 'keyfile': self.keyfile,
+ 'certfile': self.certfile,
+ 'cert_reqs': self.cert_reqs,
+ }
+ if self.ciphers:
+ if self._has_ciphers:
+ ssl_opts['ciphers'] = self.ciphers
+ else:
+ logger.warning('ciphers is specified but ignored due to old Python version')
+ return ssl.wrap_socket(sock, **ssl_opts)
+
+
+class TSSLSocket(TSocket.TSocket, TSSLBase):
+ """
+ SSL implementation of TSocket
This class creates outbound sockets wrapped using the
python standard ssl module for encrypted connections.
-
- The protocol used is set using the class variable
- SSL_VERSION, which must be one of ssl.PROTOCOL_* and
- defaults to ssl.PROTOCOL_TLSv1 for greatest security.
"""
- SSL_VERSION = ssl.PROTOCOL_TLSv1
- def __init__(self,
- host='localhost',
- port=9090,
- validate=True,
- ca_certs=None,
- keyfile=None,
- certfile=None,
- unix_socket=None,
- ciphers=None):
- """Create SSL TSocket
+ # New signature
+ # def __init__(self, host='localhost', port=9090, unix_socket=None, **ssl_args):
+ # Deprecated signature
+ # def __init__(self, host='localhost', port=9090, validate=True, ca_certs=None, keyfile=None, certfile=None, unix_socket=None, ciphers=None):
+ def __init__(self, host='localhost', port=9090, *args, **kwargs):
+ """Positional arguments: ``host``, ``port``, ``unix_socket``
- @param validate: Set to False to disable SSL certificate validation
- @type validate: bool
- @param ca_certs: Filename to the Certificate Authority pem file, possibly a
- file downloaded from: http://curl.haxx.se/ca/cacert.pem This is passed to
- the ssl_wrap function as the 'ca_certs' parameter.
- @type ca_certs: str
- @param keyfile: The private key
- @type keyfile: str
- @param certfile: The cert file
- @type certfile: str
- @param ciphers: The cipher suites to allow. This is passed to
- the ssl_wrap function as the 'ciphers' parameter.
- @type ciphers: str
-
- Raises an IOError exception if validate is True and the ca_certs file is
- None, not present or unreadable.
+ Keyword arguments: ``keyfile``, ``certfile``, ``cert_reqs``, ``ssl_version``,
+ ``ca_certs``, ``ciphers`` (Python 2.7.0 or later),
+ ``server_hostname`` (Python 2.7.9 or later)
+ Passed to ssl.wrap_socket. See ssl.wrap_socket documentation.
+
+ Alternative keywoard arguments: (Python 2.7.9 or later)
+ ``ssl_context``: ssl.SSLContext to be used for SSLContext.wrap_socket
+ ``server_hostname``: Passed to SSLContext.wrap_socket
"""
- self.validate = validate
self.is_valid = False
self.peercert = None
- if not validate:
- self.cert_reqs = ssl.CERT_NONE
- else:
- self.cert_reqs = ssl.CERT_REQUIRED
- self.ca_certs = ca_certs
- self.keyfile = keyfile
- self.certfile = certfile
- self.ciphers = ciphers
- if validate:
- if ca_certs is None or not os.access(ca_certs, os.R_OK):
- raise IOError('Certificate Authority ca_certs file "%s" '
- 'is not readable, cannot validate SSL '
- 'certificates.' % (ca_certs))
+
+ if args:
+ if len(args) > 6:
+ raise TypeError('Too many positional argument')
+ if not self._unix_socket_arg(host, port, args, kwargs):
+ self._deprecated_arg(args, kwargs, 0, 'validate')
+ self._deprecated_arg(args, kwargs, 1, 'ca_certs')
+ self._deprecated_arg(args, kwargs, 2, 'keyfile')
+ self._deprecated_arg(args, kwargs, 3, 'certfile')
+ self._deprecated_arg(args, kwargs, 4, 'unix_socket')
+ self._deprecated_arg(args, kwargs, 5, 'ciphers')
+
+ validate = kwargs.pop('validate', None)
+ if validate is not None:
+ cert_reqs_name = 'CERT_REQUIRED' if validate else 'CERT_NONE'
+ warnings.warn(
+ 'validate is deprecated. Use cert_reqs=ssl.%s instead' % cert_reqs_name,
+ DeprecationWarning)
+ if 'cert_reqs' in kwargs:
+ raise TypeError('Cannot specify both validate and cert_reqs')
+ kwargs['cert_reqs'] = ssl.CERT_REQUIRED if validate else ssl.CERT_NONE
+
+ unix_socket = kwargs.pop('unix_socket', None)
+ TSSLBase.__init__(self, False, host, kwargs)
TSocket.TSocket.__init__(self, host, port, unix_socket)
+ @property
+ def validate(self):
+ warnings.warn('Use cert_reqs instead', DeprecationWarning)
+ return self.cert_reqs != ssl.CERT_NONE
+
+ @validate.setter
+ def validate(self, value):
+ warnings.warn('Use cert_reqs instead', DeprecationWarning)
+ self.cert_reqs = ssl.CERT_REQUIRED if value else ssl.CERT_NONE
+
def open(self):
try:
res0 = self._resolveAddr()
@@ -91,29 +239,24 @@
sock_family, sock_type = res[0:2]
ip_port = res[4]
plain_sock = socket.socket(sock_family, sock_type)
- self.handle = ssl.wrap_socket(plain_sock,
- ssl_version=self.SSL_VERSION,
- do_handshake_on_connect=True,
- ca_certs=self.ca_certs,
- keyfile=self.keyfile,
- certfile=self.certfile,
- cert_reqs=self.cert_reqs,
- ciphers=self.ciphers)
+ self.handle = self._wrap_socket(plain_sock)
self.handle.settimeout(self._timeout)
try:
self.handle.connect(ip_port)
except socket.error as e:
if res is not res0[-1]:
+ logger.warning('Error while connecting with %s. Trying next one.', ip_port, exc_info=True)
continue
else:
- raise e
+ raise
break
except socket.error as e:
if self._unix_socket:
message = 'Could not connect to secure socket %s: %s' \
- % (self._unix_socket, e)
+ % (self._unix_socket, e)
else:
message = 'Could not connect to %s:%d: %s' % (self.host, self.port, e)
+ logger.error('Error while connecting with %s.', ip_port, exc_info=True)
raise TTransportException(type=TTransportException.NOT_OPEN,
message=message)
if self.validate:
@@ -161,38 +304,46 @@
'host "%s". Cert=%s' % (self.host, cert))
-class TSSLServerSocket(TSocket.TServerSocket):
+class TSSLServerSocket(TSocket.TServerSocket, TSSLBase):
"""SSL implementation of TServerSocket
This uses the ssl module's wrap_socket() method to provide SSL
negotiated encryption.
"""
- SSL_VERSION = ssl.PROTOCOL_TLSv1
- def __init__(self,
- host=None,
- port=9090,
- certfile='cert.pem',
- unix_socket=None,
- ciphers=None):
- """Initialize a TSSLServerSocket
+ # New signature
+ # def __init__(self, host='localhost', port=9090, unix_socket=None, **ssl_args):
+ # Deprecated signature
+ # def __init__(self, host=None, port=9090, certfile='cert.pem', unix_socket=None, ciphers=None):
+ def __init__(self, host=None, port=9090, *args, **kwargs):
+ """Positional arguments: ``host``, ``port``, ``unix_socket``
- @param certfile: filename of the server certificate, defaults to cert.pem
- @type certfile: str
- @param host: The hostname or IP to bind the listen socket to,
- i.e. 'localhost' for only allowing local network connections.
- Pass None to bind to all interfaces.
- @type host: str
- @param port: The port to listen on for inbound connections.
- @type port: int
- @param ciphers: The cipher suites to allow. This is passed to
- the ssl_wrap function as the 'ciphers' parameter.
- @type ciphers: str
+ Keyword arguments: ``keyfile``, ``certfile``, ``cert_reqs``, ``ssl_version``,
+ ``ca_certs``, ``ciphers`` (Python 2.7.0 or later)
+ See ssl.wrap_socket documentation.
+ Alternative keywoard arguments: (Python 2.7.9 or later)
+ ``ssl_context``: ssl.SSLContext to be used for SSLContext.wrap_socket
+ ``server_hostname``: Passed to SSLContext.wrap_socket
"""
- self.setCertfile(certfile)
- TSocket.TServerSocket.__init__(self, host, port)
- self.ciphers = ciphers
+ if args:
+ if len(args) > 3:
+ raise TypeError('Too many positional argument')
+ if not self._unix_socket_arg(host, port, args, kwargs):
+ self._deprecated_arg(args, kwargs, 0, 'certfile')
+ self._deprecated_arg(args, kwargs, 1, 'unix_socket')
+ self._deprecated_arg(args, kwargs, 2, 'ciphers')
+
+ if 'ssl_context' not in kwargs:
+ # Preserve existing behaviors for default values
+ if 'cert_reqs' not in kwargs:
+ kwargs['cert_reqs'] = ssl.CERT_NONE
+ if'certfile' not in kwargs:
+ kwargs['certfile'] = 'cert.pem'
+
+ unix_socket = kwargs.pop('unix_socket', None)
+ TSSLBase.__init__(self, True, None, kwargs)
+ TSocket.TServerSocket.__init__(self, host, port, unix_socket)
def setCertfile(self, certfile):
"""Set or change the server certificate file used to wrap new connections.
@@ -203,20 +354,18 @@
Raises an IOError exception if the certfile is not present or unreadable.
"""
- if not os.access(certfile, os.R_OK):
- raise IOError('No such certfile found: %s' % (certfile))
+ warnings.warn('Use certfile property instead.', DeprecationWarning)
self.certfile = certfile
def accept(self):
plain_client, addr = self.handle.accept()
try:
- client = ssl.wrap_socket(plain_client, certfile=self.certfile,
- server_side=True, ssl_version=self.SSL_VERSION,
- ciphers=self.ciphers)
- except ssl.SSLError as ssl_exc:
+ client = self._wrap_socket(plain_client)
+ except ssl.SSLError:
+ logger.error('Error while accepting from %s', addr, exc_info=True)
# failed handshake/ssl wrap, close socket to client
plain_client.close()
- # raise ssl_exc
+ # raise
# We can't raise the exception, because it kills most TServer derived
# serve() methods.
# Instead, return None, and let the TServer instance deal with it in
diff --git a/lib/py/test/_import_local_thrift.py b/lib/py/test/_import_local_thrift.py
new file mode 100644
index 0000000..30c1abc
--- /dev/null
+++ b/lib/py/test/_import_local_thrift.py
@@ -0,0 +1,13 @@
+import os
+import sys
+
+
+SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR)))
+
+if sys.version_info[0] == 2:
+ import glob
+ libdir = glob.glob(os.path.join(ROOT_DIR, 'lib', 'py', 'build', 'lib.*'))[0]
+ sys.path.insert(0, libdir)
+else:
+ sys.path.insert(0, os.path.join(ROOT_DIR, 'lib', 'py', 'build', 'lib'))
diff --git a/lib/py/test/test_sslsocket.py b/lib/py/test/test_sslsocket.py
new file mode 100644
index 0000000..bd2a70a
--- /dev/null
+++ b/lib/py/test/test_sslsocket.py
@@ -0,0 +1,275 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+
+import os
+import platform
+import select
+import ssl
+import sys
+import threading
+import time
+import unittest
+import warnings
+
+import _import_local_thrift
+from thrift.transport.TSSLSocket import TSSLSocket, TSSLServerSocket
+
+SCRIPT_DIR = os.path.realpath(os.path.dirname(__file__))
+ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(SCRIPT_DIR)))
+SERVER_PEM = os.path.join(ROOT_DIR, 'test', 'keys', 'server.pem')
+SERVER_CERT = os.path.join(ROOT_DIR, 'test', 'keys', 'server.crt')
+SERVER_KEY = os.path.join(ROOT_DIR, 'test', 'keys', 'server.key')
+CLIENT_CERT = os.path.join(ROOT_DIR, 'test', 'keys', 'client.crt')
+CLIENT_KEY = os.path.join(ROOT_DIR, 'test', 'keys', 'client.key')
+
+TEST_PORT = 23458
+TEST_ADDR = '/tmp/.thrift.domain.sock.%d' % TEST_PORT
+CONNECT_TIMEOUT = 10.0
+TEST_CIPHERS = 'DES-CBC3-SHA'
+
+
+class ServerAcceptor(threading.Thread):
+ def __init__(self, server):
+ super(ServerAcceptor, self).__init__()
+ self._server = server
+ self.client = None
+
+ def run(self):
+ self._server.listen()
+ self.client = self._server.accept()
+
+
+# Python 2.6 compat
+class AssertRaises(object):
+ def __init__(self, expected):
+ self._expected = expected
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ if not exc_type or not issubclass(exc_type, self._expected):
+ raise Exception('fail')
+ return True
+
+
+class TSSLSocketTest(unittest.TestCase):
+ def _assert_connection_failure(self, server, client):
+ try:
+ acc = ServerAcceptor(server)
+ acc.start()
+ time.sleep(0.15)
+ client.setTimeout(CONNECT_TIMEOUT)
+ with self._assert_raises(Exception):
+ client.open()
+ select.select([], [client.handle], [], CONNECT_TIMEOUT)
+ # self.assertIsNone(acc.client)
+ self.assertTrue(acc.client is None)
+ finally:
+ server.close()
+ client.close()
+
+ def _assert_raises(self, exc):
+ if sys.hexversion >= 0x020700F0:
+ return self.assertRaises(exc)
+ else:
+ return AssertRaises(exc)
+
+ def _assert_connection_success(self, server, client):
+ try:
+ acc = ServerAcceptor(server)
+ acc.start()
+ time.sleep(0.15)
+ client.setTimeout(CONNECT_TIMEOUT)
+ client.open()
+ select.select([], [client.handle], [], CONNECT_TIMEOUT)
+ # self.assertIsNotNone(acc.client)
+ self.assertTrue(acc.client is not None)
+ finally:
+ server.close()
+ client.close()
+
+ # deprecated feature
+ def test_deprecation(self):
+ with warnings.catch_warnings(record=True) as w:
+ warnings.filterwarnings('always', category=DeprecationWarning, module='thrift.*SSL.*')
+ TSSLSocket('localhost', TEST_PORT, validate=True, ca_certs=SERVER_CERT)
+ self.assertEqual(len(w), 1)
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.filterwarnings('always', category=DeprecationWarning, module='thrift.*SSL.*')
+ # Deprecated signature
+ # def __init__(self, host='localhost', port=9090, validate=True, ca_certs=None, keyfile=None, certfile=None, unix_socket=None, ciphers=None):
+ client = TSSLSocket('localhost', TEST_PORT, True, SERVER_CERT, CLIENT_KEY, CLIENT_CERT, None, TEST_CIPHERS)
+ self.assertEqual(len(w), 7)
+
+ with warnings.catch_warnings(record=True) as w:
+ warnings.filterwarnings('always', category=DeprecationWarning, module='thrift.*SSL.*')
+ # Deprecated signature
+ # def __init__(self, host=None, port=9090, certfile='cert.pem', unix_socket=None, ciphers=None):
+ server = TSSLServerSocket(None, TEST_PORT, SERVER_PEM, None, TEST_CIPHERS)
+ self.assertEqual(len(w), 3)
+
+ self._assert_connection_success(server, client)
+
+ # deprecated feature
+ def test_set_cert_reqs_by_validate(self):
+ c1 = TSSLSocket('localhost', TEST_PORT, validate=True, ca_certs=SERVER_CERT)
+ self.assertEqual(c1.cert_reqs, ssl.CERT_REQUIRED)
+
+ c1 = TSSLSocket('localhost', TEST_PORT, validate=False)
+ self.assertEqual(c1.cert_reqs, ssl.CERT_NONE)
+
+ # deprecated feature
+ def test_set_validate_by_cert_reqs(self):
+ c1 = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_NONE)
+ self.assertFalse(c1.validate)
+
+ c2 = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_REQUIRED, ca_certs=SERVER_CERT)
+ self.assertTrue(c2.validate)
+
+ c3 = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_OPTIONAL, ca_certs=SERVER_CERT)
+ self.assertTrue(c3.validate)
+
+ def test_unix_domain_socket(self):
+ if platform.system() == 'Windows':
+ print('skipping test_unix_domain_socket')
+ return
+ server = TSSLServerSocket(unix_socket=TEST_ADDR, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ client = TSSLSocket(None, None, TEST_ADDR, cert_reqs=ssl.CERT_NONE)
+ self._assert_connection_success(server, client)
+
+ def test_server_cert(self):
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ client = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_REQUIRED, ca_certs=SERVER_CERT)
+ self._assert_connection_success(server, client)
+
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ # server cert on in ca_certs
+ client = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_REQUIRED, ca_certs=CLIENT_CERT)
+ self._assert_connection_failure(server, client)
+
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ client = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_NONE)
+ self._assert_connection_success(server, client)
+
+ def test_set_server_cert(self):
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=CLIENT_CERT)
+ with self._assert_raises(Exception):
+ server.certfile = 'foo'
+ with self._assert_raises(Exception):
+ server.certfile = None
+ server.certfile = SERVER_CERT
+ client = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_REQUIRED, ca_certs=SERVER_CERT)
+ self._assert_connection_success(server, client)
+
+ def test_client_cert(self):
+ server = TSSLServerSocket(
+ port=TEST_PORT, cert_reqs=ssl.CERT_REQUIRED, keyfile=SERVER_KEY,
+ certfile=SERVER_CERT, ca_certs=CLIENT_CERT)
+ client = TSSLSocket('localhost', TEST_PORT, cert_reqs=ssl.CERT_NONE, certfile=CLIENT_CERT, keyfile=CLIENT_KEY)
+ self._assert_connection_success(server, client)
+
+ def test_ciphers(self):
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ciphers=TEST_CIPHERS)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ciphers=TEST_CIPHERS)
+ self._assert_connection_success(server, client)
+
+ if not TSSLSocket._has_ciphers:
+ # unittest.skip is not available for Python 2.6
+ print('skipping test_ciphers')
+ return
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ciphers='NULL')
+ self._assert_connection_failure(server, client)
+
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ciphers=TEST_CIPHERS)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ciphers='NULL')
+ self._assert_connection_failure(server, client)
+
+ def test_ssl2_and_ssl3_disabled(self):
+ if not hasattr(ssl, 'PROTOCOL_SSLv3'):
+ print('PROTOCOL_SSLv3 is not available')
+ else:
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ssl_version=ssl.PROTOCOL_SSLv3)
+ self._assert_connection_failure(server, client)
+
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ssl_version=ssl.PROTOCOL_SSLv3)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT)
+ self._assert_connection_failure(server, client)
+
+ if not hasattr(ssl, 'PROTOCOL_SSLv2'):
+ print('PROTOCOL_SSLv2 is not available')
+ else:
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ssl_version=ssl.PROTOCOL_SSLv2)
+ self._assert_connection_failure(server, client)
+
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ssl_version=ssl.PROTOCOL_SSLv2)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT)
+ self._assert_connection_failure(server, client)
+
+ def test_newer_tls(self):
+ if not TSSLSocket._has_ssl_context:
+ # unittest.skip is not available for Python 2.6
+ print('skipping test_newer_tls')
+ return
+ if not hasattr(ssl, 'PROTOCOL_TLSv1_2'):
+ print('PROTOCOL_TLSv1_2 is not available')
+ else:
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ssl_version=ssl.PROTOCOL_TLSv1_2)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ssl_version=ssl.PROTOCOL_TLSv1_2)
+ self._assert_connection_success(server, client)
+
+ if not hasattr(ssl, 'PROTOCOL_TLSv1_1'):
+ print('PROTOCOL_TLSv1_1 is not available')
+ else:
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ssl_version=ssl.PROTOCOL_TLSv1_1)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ssl_version=ssl.PROTOCOL_TLSv1_1)
+ self._assert_connection_success(server, client)
+
+ if not hasattr(ssl, 'PROTOCOL_TLSv1_1') or not hasattr(ssl, 'PROTOCOL_TLSv1_2'):
+ print('PROTOCOL_TLSv1_1 and/or PROTOCOL_TLSv1_2 is not available')
+ else:
+ server = TSSLServerSocket(port=TEST_PORT, keyfile=SERVER_KEY, certfile=SERVER_CERT, ssl_version=ssl.PROTOCOL_TLSv1_2)
+ client = TSSLSocket('localhost', TEST_PORT, ca_certs=SERVER_CERT, ssl_version=ssl.PROTOCOL_TLSv1_1)
+ self._assert_connection_failure(server, client)
+
+ def test_ssl_context(self):
+ if not TSSLSocket._has_ssl_context:
+ # unittest.skip is not available for Python 2.6
+ print('skipping test_ssl_context')
+ return
+ server_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
+ server_context.load_cert_chain(SERVER_CERT, SERVER_KEY)
+ server_context.load_verify_locations(CLIENT_CERT)
+
+ client_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH)
+ client_context.load_cert_chain(CLIENT_CERT, CLIENT_KEY)
+ client_context.load_verify_locations(SERVER_CERT)
+
+ server = TSSLServerSocket(port=TEST_PORT, ssl_context=server_context)
+ client = TSSLSocket('localhost', TEST_PORT, ssl_context=client_context)
+ self._assert_connection_success(server, client)
+
+if __name__ == '__main__':
+ # import logging
+ # logging.basicConfig(level=logging.DEBUG)
+ unittest.main()
diff --git a/lib/py/test/thrift_json.py b/lib/py/test/thrift_json.py
index 6d6c8fa..e60aaba 100644
--- a/lib/py/test/thrift_json.py
+++ b/lib/py/test/thrift_json.py
@@ -1,10 +1,29 @@
-from thrift import Thrift
-from thrift.protocol.TJSONProtocol import TJSONProtocol
-from thrift.transport import TTransport
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
import sys
import unittest
+import _import_local_thrift
+from thrift.protocol.TJSONProtocol import TJSONProtocol
+from thrift.transport import TTransport
+
#
# In order to run the test under Windows. We need to create symbolic link
# name 'thrift' to '../src' folder by using: