diff -r 67e7eda1185f Lib/ftplib.py --- a/Lib/ftplib.py Tue Apr 06 10:18:15 2010 +0200 +++ b/Lib/ftplib.py Tue Apr 06 13:37:48 2010 +0200 @@ -748,6 +748,15 @@ else: conn.close() return self.voidresp() + def quit(self): + '''Quit, and close the connection.''' + resp = self.voidcmd('QUIT') + if isinstance(self.sock, ssl.SSLSocket): + self.sock.unwrap() + self.close() + return resp + + __all__.append('FTP_TLS') all_errors = (Error, IOError, EOFError, ssl.SSLError) diff -r 67e7eda1185f Lib/ssl.py --- a/Lib/ssl.py Tue Apr 06 10:18:15 2010 +0200 +++ b/Lib/ssl.py Tue Apr 06 13:37:48 2010 +0200 @@ -78,6 +78,7 @@ from _ssl import \ from socket import socket, _fileobject, error as socket_error from socket import getnameinfo as _getnameinfo import base64 # for DER-to-PEM translation +import select class SSLSocket(socket): @@ -255,12 +256,28 @@ class SSLSocket(socket): return 0 def unwrap(self): - if self._sslobj: + if not self._sslobj: + raise ValueError("No SSL wrapper around " + str(self)) + timeout = self.gettimeout() + if timeout == 0: + # Non-blocking, leave the upper layers handle retries s = self._sslobj.shutdown() self._sslobj = None return s - else: - raise ValueError("No SSL wrapper around " + str(self)) + fd = self.fileno() + while True: + try: + s = self._sslobj.shutdown() + self._sslobj = None + return s + except SSLError as err: + # In blocking mode, we retry as long as the socket condition + # changes before the timeout. + if err.args[0] in (SSL_ERROR_WANT_READ, + SSL_ERROR_WANT_WRITE): + if select.select([fd], [fd], [fd], timeout): + continue + raise def shutdown(self, how): self._sslobj = None diff -r 67e7eda1185f Lib/test/test_ftplib.py --- a/Lib/test/test_ftplib.py Tue Apr 06 10:18:15 2010 +0200 +++ b/Lib/test/test_ftplib.py Tue Apr 06 13:37:48 2010 +0200 @@ -29,6 +29,7 @@ NLST_DATA = 'foo\r\nbar\r\n' class DummyDTPHandler(asynchat.async_chat): + dtp_conn_closed = False def __init__(self, conn, baseclass): asynchat.async_chat.__init__(self, conn) @@ -39,8 +40,13 @@ class DummyDTPHandler(asynchat.async_cha self.baseclass.last_received_data += self.recv(1024) def handle_close(self): - self.baseclass.push('226 transfer complete') - self.close() + # XXX: this method can be called many times in a row for a single + # connection, including in clear-text (non-TLS) mode. + # (behaviour witnessed with test_data_connection) + if not self.dtp_conn_closed: + self.baseclass.push('226 transfer complete') + self.close() + self.dtp_conn_closed = True class DummyFTPHandler(asynchat.async_chat): @@ -253,6 +259,7 @@ if ssl is not None: """An asyncore.dispatcher subclass supporting TLS/SSL.""" _ssl_accepting = False + _ssl_closing = False def secure_connection(self): self.socket = ssl.wrap_socket(self.socket, suppress_ragged_eofs=False, @@ -277,15 +284,37 @@ if ssl is not None: else: self._ssl_accepting = False + def _do_ssl_shutdown(self): + self._ssl_closing = True + try: + self.socket = self.socket.unwrap() + except ssl.SSLError, err: + if err.args[0] in (ssl.SSL_ERROR_WANT_READ, + ssl.SSL_ERROR_WANT_WRITE): + return + raise + except socket.error, err: + if err.args[0] not in (errno.EPIPE, errno.EBADF, + errno.ECONNRESET, errno.ENOTCONN, + errno.ESHUTDOWN, errno.ECONNABORTED): + raise + # Socket already closed => just bail out gracefully + self._ssl_closing = False + super(SSLConnection, self).close() + def handle_read_event(self): if self._ssl_accepting: self._do_ssl_handshake() + elif self._ssl_closing: + self._do_ssl_shutdown() else: super(SSLConnection, self).handle_read_event() def handle_write_event(self): if self._ssl_accepting: self._do_ssl_handshake() + elif self._ssl_closing: + self._do_ssl_shutdown() else: super(SSLConnection, self).handle_write_event() @@ -315,12 +344,9 @@ if ssl is not None: raise def close(self): - try: - if isinstance(self.socket, ssl.SSLSocket): - if self.socket._sslobj is not None: - self.socket.unwrap() - finally: - super(SSLConnection, self).close() + if (isinstance(self.socket, ssl.SSLSocket) and + self.socket._sslobj is not None): + self._do_ssl_shutdown() class DummyTLS_DTPHandler(SSLConnection, DummyDTPHandler): @@ -597,21 +623,21 @@ class TestTLS_FTPClass(TestCase): sock = self.client.transfercmd('list') self.assertNotIsInstance(sock, ssl.SSLSocket) sock.close() - self.client.voidresp() + self.assertEqual(self.client.voidresp(), "226 transfer complete") # secured, after PROT P self.client.prot_p() sock = self.client.transfercmd('list') self.assertIsInstance(sock, ssl.SSLSocket) sock.close() - self.client.voidresp() + self.assertEqual(self.client.voidresp(), "226 transfer complete") # PROT C is issued, the connection must be in cleartext again self.client.prot_c() sock = self.client.transfercmd('list') self.assertNotIsInstance(sock, ssl.SSLSocket) sock.close() - self.client.voidresp() + self.assertEqual(self.client.voidresp(), "226 transfer complete") def test_login(self): # login() is supposed to implicitly secure the control connection diff -r 67e7eda1185f Modules/_ssl.c --- a/Modules/_ssl.c Tue Apr 06 10:18:15 2010 +0200 +++ b/Modules/_ssl.c Tue Apr 06 13:37:48 2010 +0200 @@ -1362,12 +1362,19 @@ static PyObject *PySSL_SSLshutdown(PySSL } PySSL_END_ALLOW_THREADS - if (err < 0) + /* Issue #8108: "fake" error returns have been witnessed with + OpenSSL >= 0.9.8m ("errno 0"). + Also witnessed by LightHTTPd: + http://redmine.lighttpd.net/boards/2/topics/2779 + */ + if (err < 0) { + if (err != -1 + || SSL_get_error(self->ssl, err) != SSL_ERROR_SYSCALL + || ERR_get_error() != 0 || errno != 0) return PySSL_SetError(self, err, __FILE__, __LINE__); - else { - Py_INCREF(self->Socket); - return (PyObject *) (self->Socket); } + Py_INCREF(self->Socket); + return (PyObject *) (self->Socket); } PyDoc_STRVAR(PySSL_SSLshutdown_doc,