Skip to content

Commit 3ab8723

Browse files
committed
Make handlers.get_ssl_context non-static to support multi-threaded environment
1 parent c224708 commit 3ab8723

File tree

5 files changed

+120
-184
lines changed

5 files changed

+120
-184
lines changed

pyftpdlib/handlers.py

Lines changed: 25 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3455,19 +3455,10 @@ def __init__(self, conn, server, ioloop=None):
34553455
self._pbsz = False
34563456
self._prot = False
34573457
self.ssl_context = self.get_ssl_context()
3458-
if self.client_certfile is not None:
3459-
from OpenSSL.SSL import VERIFY_CLIENT_ONCE
3460-
from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT
3461-
from OpenSSL.SSL import VERIFY_PEER
3462-
self.ssl_context.set_verify(VERIFY_PEER |
3463-
VERIFY_FAIL_IF_NO_PEER_CERT |
3464-
VERIFY_CLIENT_ONCE,
3465-
self.verify_certs_callback)
34663458

34673459
def __repr__(self):
34683460
return FTPHandler.__repr__(self)
34693461

3470-
# Cannot be @classmethod, need instance to log
34713462
def verify_certs_callback(self, connection, x509,
34723463
errnum, errdepth, ok):
34733464
if not ok:
@@ -3476,29 +3467,36 @@ def verify_certs_callback(self, connection, x509,
34763467
self.log("Client certificate is valid.")
34773468
return ok
34783469

3479-
@classmethod
3480-
def get_ssl_context(cls):
3481-
if cls.ssl_context is None:
3482-
if cls.certfile is None:
3470+
def get_ssl_context(self):
3471+
if self.ssl_context is None:
3472+
if self.certfile is None:
34833473
raise ValueError("at least certfile must be specified")
3484-
cls.ssl_context = SSL.Context(cls.ssl_protocol)
3485-
if cls.ssl_protocol != SSL.SSLv2_METHOD:
3486-
cls.ssl_context.set_options(SSL.OP_NO_SSLv2)
3474+
self.ssl_context = SSL.Context(self.ssl_protocol)
3475+
if self.ssl_protocol != SSL.SSLv2_METHOD:
3476+
self.ssl_context.set_options(SSL.OP_NO_SSLv2)
34873477
else:
34883478
warnings.warn("SSLv2 protocol is insecure", RuntimeWarning)
3489-
cls.ssl_context.use_certificate_chain_file(cls.certfile)
3490-
if not cls.keyfile:
3491-
cls.keyfile = cls.certfile
3492-
cls.ssl_context.use_privatekey_file(cls.keyfile)
3493-
if cls.client_certfile is not None:
3479+
self.ssl_context.use_certificate_chain_file(self.certfile)
3480+
if not self.keyfile:
3481+
self.keyfile = self.certfile
3482+
self.ssl_context.use_privatekey_file(self.keyfile)
3483+
if self.client_certfile is not None:
3484+
from OpenSSL.SSL import VERIFY_CLIENT_ONCE
3485+
from OpenSSL.SSL import VERIFY_FAIL_IF_NO_PEER_CERT
3486+
from OpenSSL.SSL import VERIFY_PEER
3487+
self.ssl_context.set_verify(VERIFY_PEER |
3488+
VERIFY_FAIL_IF_NO_PEER_CERT |
3489+
VERIFY_CLIENT_ONCE,
3490+
self.verify_certs_callback)
34943491
from OpenSSL.SSL import OP_NO_TICKET
34953492
from OpenSSL.SSL import SESS_CACHE_OFF
3496-
cls.ssl_context.load_verify_locations(cls.client_certfile)
3497-
cls.ssl_context.set_session_cache_mode(SESS_CACHE_OFF)
3498-
cls.ssl_options = cls.ssl_options | OP_NO_TICKET
3499-
if cls.ssl_options:
3500-
cls.ssl_context.set_options(cls.ssl_options)
3501-
return cls.ssl_context
3493+
self.ssl_context.load_verify_locations(
3494+
self.client_certfile)
3495+
self.ssl_context.set_session_cache_mode(SESS_CACHE_OFF)
3496+
self.ssl_options = self.ssl_options | OP_NO_TICKET
3497+
if self.ssl_options:
3498+
self.ssl_context.set_options(self.ssl_options)
3499+
return self.ssl_context
35023500

35033501
# --- overridden methods
35043502

pyftpdlib/servers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,8 @@ def __init__(self, address_or_socket, handler, ioloop=None, backlog=100):
104104
self.ip_map = []
105105
# in case of FTPS class not properly configured we want errors
106106
# to be raised here rather than later, when client connects
107-
if hasattr(handler, 'get_ssl_context'):
108-
handler.get_ssl_context()
107+
# if hasattr(handler, 'get_ssl_context'):
108+
# handler.get_ssl_context(handler)
109109
if callable(getattr(address_or_socket, 'listen', None)):
110110
sock = address_or_socket
111111
sock.setblocking(0)

pyftpdlib/test/functional_ssl_client_certfile_tests.py

Lines changed: 0 additions & 154 deletions
This file was deleted.

pyftpdlib/test/test_functional.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2132,7 +2132,7 @@ class _TestNetworkProtocols(object):
21322132
HOST = HOST
21332133

21342134
def setUp(self):
2135-
self.server = self.server_class((self.HOST, 0))
2135+
self.server = self.server_class(addr=(self.HOST, 0))
21362136
self.server.start()
21372137
self.client = self.client_class(timeout=TIMEOUT)
21382138
self.client.connect(self.server.host, self.server.port)

pyftpdlib/test/test_functional_ssl.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import socket
1111
import sys
1212
import ssl
13+
from ssl import SSLError
1314

1415
import OpenSSL # requires "pip install pyopenssl"
1516

@@ -49,6 +50,8 @@
4950

5051
CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__),
5152
'keycert.pem'))
53+
CLIENT_CERTFILE = os.path.abspath(os.path.join(os.path.dirname(__file__),
54+
'clientcert.pem'))
5255

5356
del OpenSSL
5457

@@ -78,6 +81,13 @@ class FTPSServer(ThreadedTestFTPd):
7881
handler = TLS_FTPHandler
7982
handler.certfile = CERTFILE
8083

84+
def __init__(self, use_client_cert=False, *args, **kwargs):
85+
if use_client_cert:
86+
self.handler.client_certfile = CLIENT_CERTFILE
87+
else:
88+
self.handler.client_certfile = None
89+
super(FTPSServer, self).__init__(*args, **kwargs)
90+
8191
class TLSTestMixin:
8292
server_class = FTPSServer
8393
client_class = FTPSClient
@@ -408,6 +418,88 @@ def test_sslv2(self):
408418
self.client.ssl_version = ssl.PROTOCOL_SSLv2
409419

410420

421+
@unittest.skipUnless(FTPS_SUPPORT, FTPS_UNSUPPORT_REASON)
422+
class TestClientFTPS(unittest.TestCase):
423+
"""Specific tests for TLS_FTPHandler class."""
424+
425+
def setUp(self):
426+
self.server = FTPSServer(use_client_cert=True)
427+
self.server.start()
428+
429+
def tearDown(self):
430+
self.client.ssl_version = ssl.PROTOCOL_SSLv23
431+
with self.server.lock:
432+
self.server.handler.ssl_version = ssl.PROTOCOL_SSLv23
433+
self.server.handler.tls_control_required = False
434+
self.server.handler.tls_data_required = False
435+
self.server.handler.client_certfile = None
436+
self.client.close()
437+
self.server.stop()
438+
439+
def assertRaisesWithMsg(self, excClass, msg, callableObj, *args, **kwargs):
440+
try:
441+
callableObj(*args, **kwargs)
442+
except excClass as err:
443+
if str(err) == msg:
444+
return
445+
raise self.failureException("%s != %s" % (str(err), msg))
446+
else:
447+
if hasattr(excClass, '__name__'):
448+
excName = excClass.__name__
449+
else:
450+
excName = str(excClass)
451+
raise self.failureException("%s not raised" % excName)
452+
453+
@classmethod
454+
def get_ssl_context(cls, certfile):
455+
ssl_context = ssl.create_default_context()
456+
ssl_context.check_hostname = False
457+
ssl_context.verify_mode = ssl.CERT_NONE
458+
if certfile:
459+
ssl_context.load_cert_chain(certfile)
460+
return ssl_context
461+
462+
def test_auth_client_cert(self):
463+
ctx = self.get_ssl_context(CLIENT_CERTFILE)
464+
self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx)
465+
self.client.connect(self.server.host, self.server.port)
466+
# secured
467+
try:
468+
self.client.login()
469+
except Exception:
470+
self.fail("login with certificate should work")
471+
472+
def test_auth_client_nocert(self):
473+
self.client = ftplib.FTP_TLS(timeout=TIMEOUT)
474+
self.client.connect(self.server.host, self.server.port)
475+
try:
476+
self.client.login()
477+
except SSLError as e:
478+
# client should not be able to log in
479+
if "SSLV3_ALERT_HANDSHAKE_FAILURE" in e.reason:
480+
pass
481+
else:
482+
self.fail("Incorrect SSL error with" +
483+
" missing client certificate")
484+
else:
485+
self.fail("Client able to log in with no certificate")
486+
487+
def test_auth_client_badcert(self):
488+
ctx = self.get_ssl_context(CERTFILE)
489+
self.client = ftplib.FTP_TLS(timeout=TIMEOUT, context=ctx)
490+
self.client.connect(self.server.host, self.server.port)
491+
try:
492+
self.client.login()
493+
except Exception as e:
494+
# client should not be able to log in
495+
if "TLSV1_ALERT_UNKNOWN_CA" in e.reason:
496+
pass
497+
else:
498+
self.fail("Incorrect SSL error with bad client certificate")
499+
else:
500+
self.fail("Client able to log in with bad certificate")
501+
502+
411503
configure_logging()
412504
remove_test_files()
413505

0 commit comments

Comments
 (0)