Skip to content

Commit

Permalink
Make handlers.get_ssl_context non-static to support multi-threaded en…
Browse files Browse the repository at this point in the history
…vironment
  • Loading branch information
zuohaocheng committed Jul 17, 2017
1 parent c224708 commit 3ab8723
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 184 deletions.
52 changes: 25 additions & 27 deletions pyftpdlib/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions pyftpdlib/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
154 changes: 0 additions & 154 deletions pyftpdlib/test/functional_ssl_client_certfile_tests.py

This file was deleted.

2 changes: 1 addition & 1 deletion pyftpdlib/test/test_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
92 changes: 92 additions & 0 deletions pyftpdlib/test/test_functional_ssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import socket
import sys
import ssl
from ssl import SSLError

import OpenSSL # requires "pip install pyopenssl"

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down

0 comments on commit 3ab8723

Please sign in to comment.