From 3ab87234bdfedfa473706ea31c8ab87e41e6fcf0 Mon Sep 17 00:00:00 2001 From: Zuo Haocheng Date: Mon, 17 Jul 2017 15:03:22 +0800 Subject: [PATCH] Make handlers.get_ssl_context non-static to support multi-threaded environment --- pyftpdlib/handlers.py | 52 +++--- pyftpdlib/servers.py | 4 +- .../functional_ssl_client_certfile_tests.py | 154 ------------------ pyftpdlib/test/test_functional.py | 2 +- pyftpdlib/test/test_functional_ssl.py | 92 +++++++++++ 5 files changed, 120 insertions(+), 184 deletions(-) delete mode 100644 pyftpdlib/test/functional_ssl_client_certfile_tests.py diff --git a/pyftpdlib/handlers.py b/pyftpdlib/handlers.py index cd777dc4..a3883658 100644 --- a/pyftpdlib/handlers.py +++ b/pyftpdlib/handlers.py @@ -3455,19 +3455,10 @@ def __init__(self, conn, server, ioloop=None): self._pbsz = False self._prot = False self.ssl_context = self.get_ssl_context() - if self.client_certfile is not None: - from OpenSSL.SSL import VERIFY_CLIENT_ONCE - from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT - from OpenSSL.SSL import VERIFY_PEER - self.ssl_context.set_verify(VERIFY_PEER | - VERIFY_FAIL_IF_NO_PEER_CERT | - VERIFY_CLIENT_ONCE, - self.verify_certs_callback) def __repr__(self): return FTPHandler.__repr__(self) - # Cannot be @classmethod, need instance to log def verify_certs_callback(self, connection, x509, errnum, errdepth, ok): if not ok: @@ -3476,29 +3467,36 @@ def verify_certs_callback(self, connection, x509, self.log("Client certificate is valid.") return ok - @classmethod - def get_ssl_context(cls): - if cls.ssl_context is None: - if cls.certfile is None: + def get_ssl_context(self): + if self.ssl_context is None: + if self.certfile is None: raise ValueError("at least certfile must be specified") - cls.ssl_context = SSL.Context(cls.ssl_protocol) - if cls.ssl_protocol != SSL.SSLv2_METHOD: - cls.ssl_context.set_options(SSL.OP_NO_SSLv2) + self.ssl_context = SSL.Context(self.ssl_protocol) + if self.ssl_protocol != SSL.SSLv2_METHOD: + self.ssl_context.set_options(SSL.OP_NO_SSLv2) else: warnings.warn("SSLv2 protocol is insecure", RuntimeWarning) - cls.ssl_context.use_certificate_chain_file(cls.certfile) - if not cls.keyfile: - cls.keyfile = cls.certfile - cls.ssl_context.use_privatekey_file(cls.keyfile) - if cls.client_certfile is not None: + self.ssl_context.use_certificate_chain_file(self.certfile) + if not self.keyfile: + self.keyfile = self.certfile + self.ssl_context.use_privatekey_file(self.keyfile) + if self.client_certfile is not None: + from OpenSSL.SSL import VERIFY_CLIENT_ONCE + from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT + from OpenSSL.SSL import VERIFY_PEER + self.ssl_context.set_verify(VERIFY_PEER | + VERIFY_FAIL_IF_NO_PEER_CERT | + VERIFY_CLIENT_ONCE, + self.verify_certs_callback) from OpenSSL.SSL import OP_NO_TICKET from OpenSSL.SSL import SESS_CACHE_OFF - cls.ssl_context.load_verify_locations(cls.client_certfile) - cls.ssl_context.set_session_cache_mode(SESS_CACHE_OFF) - cls.ssl_options = cls.ssl_options | OP_NO_TICKET - if cls.ssl_options: - cls.ssl_context.set_options(cls.ssl_options) - return cls.ssl_context + self.ssl_context.load_verify_locations( + self.client_certfile) + self.ssl_context.set_session_cache_mode(SESS_CACHE_OFF) + self.ssl_options = self.ssl_options | OP_NO_TICKET + if self.ssl_options: + self.ssl_context.set_options(self.ssl_options) + return self.ssl_context # --- overridden methods diff --git a/pyftpdlib/servers.py b/pyftpdlib/servers.py index e703af3f..af623439 100644 --- a/pyftpdlib/servers.py +++ b/pyftpdlib/servers.py @@ -104,8 +104,8 @@ def __init__(self, address_or_socket, handler, ioloop=None, backlog=100): self.ip_map = [] # in case of FTPS class not properly configured we want errors # to be raised here rather than later, when client connects - if hasattr(handler, 'get_ssl_context'): - handler.get_ssl_context() + # if hasattr(handler, 'get_ssl_context'): + # handler.get_ssl_context(handler) if callable(getattr(address_or_socket, 'listen', None)): sock = address_or_socket sock.setblocking(0) diff --git a/pyftpdlib/test/functional_ssl_client_certfile_tests.py b/pyftpdlib/test/functional_ssl_client_certfile_tests.py deleted file mode 100644 index 7c5d93c5..00000000 --- a/pyftpdlib/test/functional_ssl_client_certfile_tests.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python - -# Copyright (C) 2007-2016 Giampaolo Rodola' . -# Use of this source code is governed by MIT license that can be -# found in the LICENSE file. -# -# -# Does not follow naming convention of other tests because this -# CANNOT be run in the same test suite with test_functional_ssl. -# The test parallelism causes SSL errors when there should be none -# Please run these tests separately - -import ftplib -import os -import sys - -import OpenSSL # requires "pip install pyopenssl" - -from pyftpdlib.handlers import TLS_FTPHandler -from pyftpdlib.test import configure_logging -from pyftpdlib.test import remove_test_files -from pyftpdlib.test import ThreadedTestFTPd -from pyftpdlib.test import TIMEOUT -from pyftpdlib.test import unittest -from pyftpdlib.test import VERBOSITY -from _ssl import SSLError -import ssl - - -FTPS_SUPPORT = hasattr(ftplib, 'FTP_TLS') -if sys.version_info < (2, 7): - FTPS_UNSUPPORT_REASON = "requires python 2.7+" -else: - FTPS_UNSUPPORT_REASON = "FTPS test skipped" - -CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'keycert.pem')) -CLIENT_CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'clientcert.pem')) - -del OpenSSL - - -if FTPS_SUPPORT: - class FTPSClient(ftplib.FTP_TLS): - """A modified version of ftplib.FTP_TLS class which implicitly - secure the data connection after login(). - """ - - def login(self, *args, **kwargs): - ftplib.FTP_TLS.login(self, *args, **kwargs) - self.prot_p() - - class FTPSServerAuth(ThreadedTestFTPd): - """A threaded FTPS server that forces client certificate - authentication used for functional testing. - """ - handler = TLS_FTPHandler - handler.certfile = CERTFILE - handler.client_certfile = CLIENT_CERTFILE - - -# ===================================================================== -# dedicated FTPS tests with client authentication -# ===================================================================== - - -@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) -class TestFTPS(unittest.TestCase): - """Specific tests for TLS_FTPHandler class.""" - - def setUp(self): - self.server = FTPSServerAuth() - self.server.start() - - def tearDown(self): - self.client.ssl_version = ssl.PROTOCOL_SSLv23 - with self.server.lock: - self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 - self.server.handler.tls_control_required = False - self.server.handler.tls_data_required = False - self.client.close() - self.server.stop() - - def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): - try: - callableObj(*args, **kwargs) - except excClass as err: - if str(err) == msg: - return - raise self.failureException("%s != %s" % (str(err), msg)) - else: - if hasattr(excClass, '__name__'): - excName = excClass.__name__ - else: - excName = str(excClass) - raise self.failureException("%s not raised" % excName) - - @classmethod - def get_ssl_context(cls, certfile): - ssl_context = ssl.create_default_context() - ssl_context.check_hostname = False - ssl_context.verify_mode = ssl.CERT_NONE - if certfile: - ssl_context.load_cert_chain(certfile) - return ssl_context - - def test_auth_client_cert(self): - ctx = self.get_ssl_context(CLIENT_CERTFILE) - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) - self.client.connect(self.server.host, self.server.port) - # secured - try: - self.client.login() - except Exception: - self.fail("login with certificate should work") - - def test_auth_client_nocert(self): - self.client = ftplib.FTP_TLS(timeout=TIMEOUT) - self.client.connect(self.server.host, self.server.port) - try: - self.client.login() - except SSLError as e: - # client should not be able to log in - if "SSLV3_ALERT_HANDSHAKE_FAILURE" in e.reason: - pass - else: - self.fail("Incorrect SSL error with" + - " missing client certificate") - else: - self.fail("Client able to log in with no certificate") - - def test_auth_client_badcert(self): - ctx = self.get_ssl_context(CERTFILE) - self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) - self.client.connect(self.server.host, self.server.port) - try: - self.client.login() - except Exception as e: - # client should not be able to log in - if "TLSV1_ALERT_UNKNOWN_CA" in e.reason: - pass - else: - self.fail("Incorrect SSL error with bad client certificate") - else: - self.fail("Client able to log in with bad certificate") - - -configure_logging() -remove_test_files() - - -if __name__ == '__main__': - unittest.main(verbosity=VERBOSITY) diff --git a/pyftpdlib/test/test_functional.py b/pyftpdlib/test/test_functional.py index 496cf93c..9c7214a4 100644 --- a/pyftpdlib/test/test_functional.py +++ b/pyftpdlib/test/test_functional.py @@ -2132,7 +2132,7 @@ class _TestNetworkProtocols(object): HOST = HOST def setUp(self): - self.server = self.server_class((self.HOST, 0)) + self.server = self.server_class(addr=(self.HOST, 0)) self.server.start() self.client = self.client_class(timeout=TIMEOUT) self.client.connect(self.server.host, self.server.port) diff --git a/pyftpdlib/test/test_functional_ssl.py b/pyftpdlib/test/test_functional_ssl.py index c80b1e7e..2fee34e0 100644 --- a/pyftpdlib/test/test_functional_ssl.py +++ b/pyftpdlib/test/test_functional_ssl.py @@ -10,6 +10,7 @@ import socket import sys import ssl +from ssl import SSLError import OpenSSL # requires "pip install pyopenssl" @@ -49,6 +50,8 @@ CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), 'keycert.pem')) +CLIENT_CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__), + 'clientcert.pem')) del OpenSSL @@ -78,6 +81,13 @@ class FTPSServer(ThreadedTestFTPd): handler = TLS_FTPHandler handler.certfile = CERTFILE + def __init__(self, use_client_cert=False, *args, **kwargs): + if use_client_cert: + self.handler.client_certfile = CLIENT_CERTFILE + else: + self.handler.client_certfile = None + super(FTPSServer, self).__init__(*args, **kwargs) + class TLSTestMixin: server_class = FTPSServer client_class = FTPSClient @@ -408,6 +418,88 @@ def test_sslv2(self): self.client.ssl_version = ssl.PROTOCOL_SSLv2 +@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON) +class TestClientFTPS(unittest.TestCase): + """Specific tests for TLS_FTPHandler class.""" + + def setUp(self): + self.server = FTPSServer(use_client_cert=True) + self.server.start() + + def tearDown(self): + self.client.ssl_version = ssl.PROTOCOL_SSLv23 + with self.server.lock: + self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23 + self.server.handler.tls_control_required = False + self.server.handler.tls_data_required = False + self.server.handler.client_certfile = None + self.client.close() + self.server.stop() + + def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs): + try: + callableObj(*args, **kwargs) + except excClass as err: + if str(err) == msg: + return + raise self.failureException("%s != %s" % (str(err), msg)) + else: + if hasattr(excClass, '__name__'): + excName = excClass.__name__ + else: + excName = str(excClass) + raise self.failureException("%s not raised" % excName) + + @classmethod + def get_ssl_context(cls, certfile): + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + if certfile: + ssl_context.load_cert_chain(certfile) + return ssl_context + + def test_auth_client_cert(self): + ctx = self.get_ssl_context(CLIENT_CERTFILE) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) + self.client.connect(self.server.host, self.server.port) + # secured + try: + self.client.login() + except Exception: + self.fail("login with certificate should work") + + def test_auth_client_nocert(self): + self.client = ftplib.FTP_TLS(timeout=TIMEOUT) + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except SSLError as e: + # client should not be able to log in + if "SSLV3_ALERT_HANDSHAKE_FAILURE" in e.reason: + pass + else: + self.fail("Incorrect SSL error with" + + " missing client certificate") + else: + self.fail("Client able to log in with no certificate") + + def test_auth_client_badcert(self): + ctx = self.get_ssl_context(CERTFILE) + self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx) + self.client.connect(self.server.host, self.server.port) + try: + self.client.login() + except Exception as e: + # client should not be able to log in + if "TLSV1_ALERT_UNKNOWN_CA" in e.reason: + pass + else: + self.fail("Incorrect SSL error with bad client certificate") + else: + self.fail("Client able to log in with bad certificate") + + configure_logging() remove_test_files()