diff -r a1f2a6e0a978 Doc/library/ftplib.rst --- a/Doc/library/ftplib.rst Sat May 21 09:37:36 2011 -0700 +++ b/Doc/library/ftplib.rst Sat May 21 20:06:49 2011 +0200 @@ -426,6 +426,14 @@ Set up secure control connection by using TLS or SSL, depending on what specified in :meth:`ssl_version` attribute. +.. method:: FTP_TLS.ccc() + + Revert control channel back to plaintex. This can be useful to take + advantage of firewalls that know how to handle NAT with non-secure FTP + without opening fixed ports. + + .. versionadded:: 3.3 + .. method:: FTP_TLS.prot_p() Set up secure data connection. diff -r a1f2a6e0a978 Lib/ftplib.py --- a/Lib/ftplib.py Sat May 21 09:37:36 2011 -0700 +++ b/Lib/ftplib.py Sat May 21 20:06:49 2011 +0200 @@ -708,6 +708,14 @@ self.file = self.sock.makefile(mode='r', encoding=self.encoding) return resp + def ccc(self): + '''Switch back to a clear-text control connection.''' + if not isinstance(self.sock, ssl.SSLSocket): + raise ValueError("not using TLS") + resp = self.voidcmd('CCC') + self.sock = self.sock.unwrap() + return resp + def prot_p(self): '''Set up secure data connection.''' # PROT defines whether or not the data channel is to be protected. diff -r a1f2a6e0a978 Lib/test/test_ftplib.py --- a/Lib/test/test_ftplib.py Sat May 21 09:37:36 2011 -0700 +++ b/Lib/test/test_ftplib.py Sat May 21 20:06:49 2011 +0200 @@ -303,11 +303,11 @@ _ssl_closing = False def secure_connection(self): - self.del_channel() socket = ssl.wrap_socket(self.socket, suppress_ragged_eofs=False, certfile=CERTFILE, server_side=True, do_handshake_on_connect=False, ssl_version=ssl.PROTOCOL_SSLv23) + self.del_channel() self.set_socket(socket) self._ssl_accepting = True @@ -342,7 +342,10 @@ # http://www.mail-archive.com/openssl-users@openssl.org/msg60710.html pass self._ssl_closing = False - super(SSLConnection, self).close() + if getattr(self, '_ccc', False) == False: + super(SSLConnection, self).close() + else: + pass def handle_read_event(self): if self._ssl_accepting: @@ -410,12 +413,18 @@ def __init__(self, conn): DummyFTPHandler.__init__(self, conn) self.secure_data_channel = False + self._ccc = False def cmd_auth(self, line): """Set up secure control channel.""" self.push('234 AUTH TLS successful') self.secure_connection() + def cmd_ccc(self, line): + self.push('220 Reverting back to clear-text') + self._ccc = True + self._do_ssl_shutdown() + def cmd_pbsz(self, line): """Negotiate size of buffer for secure data transfer. For TLS/SSL the only valid value for the parameter is '0'. @@ -872,6 +881,15 @@ self.assertIs(sock.context, ctx) self.assertIsInstance(sock, ssl.SSLSocket) + def test_ccc(self): + self.assertRaises(ValueError, self.client.ccc) + self.client.login(secure=True) + self.assertIsInstance(self.client.sock, ssl.SSLSocket) + self.client.ccc() + self.assertRaises(ValueError, self.client.sock.unwrap) + self.client.sendcmd('noop') + self.client.quit() + class TestTimeouts(TestCase):