Index: Doc/library/ftplib.rst =================================================================== --- Doc/library/ftplib.rst (revisione 81495) +++ Doc/library/ftplib.rst (copia locale) @@ -65,7 +65,7 @@ Support for the :keyword:`with` statement was added. -.. class:: FTP_TLS(host='', user='', passwd='', acct='', [keyfile[, certfile[, timeout]]]) +.. class:: FTP_TLS(host='', user='', passwd='', acct='', [keyfile[, certfile[, context[, timeout]]]]) A :class:`FTP` subclass which adds TLS support to FTP as described in :rfc:`4217`. @@ -74,6 +74,9 @@ explicitly ask for it by calling the :meth:`prot_p` method. *keyfile* and *certfile* are optional -- they can contain a PEM formatted private key and certificate chain file name for the SSL connection. + *context* parameter is a :class:`ssl.SSLContext` object which allows + bundling SSL configuration options, certificates and private keys into a + single (potentially long-lived) structure. .. versionadded:: 3.2 Index: Lib/ftplib.py =================================================================== --- Lib/ftplib.py (revisione 81495) +++ Lib/ftplib.py (copia locale) @@ -638,9 +638,17 @@ ssl_version = ssl.PROTOCOL_TLSv1 def __init__(self, host='', user='', passwd='', acct='', keyfile=None, - certfile=None, timeout=_GLOBAL_DEFAULT_TIMEOUT): + certfile=None, context=None, + timeout=_GLOBAL_DEFAULT_TIMEOUT): + if context is not None and keyfile is not None: + raise ValueError("context and keyfile arguments are mutually " + "exclusive") + if context is not None and certfile is not None: + raise ValueError("context and certfile arguments are mutually " + "exclusive") self.keyfile = keyfile self.certfile = certfile + self.context = context self._prot_p = False FTP.__init__(self, host, user, passwd, acct, timeout) @@ -657,8 +665,12 @@ resp = self.voidcmd('AUTH TLS') else: resp = self.voidcmd('AUTH SSL') - self.sock = ssl.wrap_socket(self.sock, self.keyfile, self.certfile, - ssl_version=self.ssl_version) + if self.context is not None: + self.sock = self.context.wrap_socket(self.sock) + else: + self.sock = ssl.wrap_socket(self.sock, self.keyfile, + self.certfile, + ssl_version=self.ssl_version) self.file = self.sock.makefile(mode='r', encoding=self.encoding) return resp @@ -689,8 +701,11 @@ def ntransfercmd(self, cmd, rest=None): conn, size = FTP.ntransfercmd(self, cmd, rest) if self._prot_p: - conn = ssl.wrap_socket(conn, self.keyfile, self.certfile, - ssl_version=self.ssl_version) + if self.context is not None: + conn = self.context.wrap_socket(conn) + else: + conn = ssl.wrap_socket(conn, self.keyfile, self.certfile, + ssl_version=self.ssl_version) return conn, size def retrbinary(self, cmd, callback, blocksize=8192, rest=None): Index: Lib/test/test_ftplib.py =================================================================== --- Lib/test/test_ftplib.py (revisione 81495) +++ Lib/test/test_ftplib.py (copia locale) @@ -719,7 +719,28 @@ finally: self.client.ssl_version = ssl.PROTOCOL_TLSv1 + def test_context(self): + self.client.quit() + ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + self.assertRaises(ValueError, ftplib.FTP_TLS, keyfile=CERTFILE, + context=ctx) + self.assertRaises(ValueError, ftplib.FTP_TLS, certfile=CERTFILE, + context=ctx) + self.assertRaises(ValueError, ftplib.FTP_TLS, certfile=CERTFILE, + keyfile=CERTFILE, context=ctx) + self.client = ftplib.FTP_TLS(context=ctx, timeout=2) + self.client.connect(self.server.host, self.server.port) + self.assertNotIsInstance(self.client.sock, ssl.SSLSocket) + self.client.auth() + self.assertIsInstance(self.client.sock, ssl.SSLSocket) + + self.client.prot_p() + sock = self.client.transfercmd('list') + self.assertIsInstance(sock, ssl.SSLSocket) + sock.close() + + class TestTimeouts(TestCase): def setUp(self):