From 41032a69c12c1d4939daa84c98b2726f00380a9e Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Thu, 27 Oct 2011 23:56:55 +0200 Subject: [PATCH] Issue #11183: Add finer-grained exceptions to the ssl module, so that you don't have to inspect the exception's attributes in the common case. --- Doc/library/ssl.rst | 42 +++++++++++++++++++++++++++++++ Lib/ssl.py | 6 ++++- Lib/test/test_ssl.py | 33 ++++++++++-------------- Misc/NEWS | 3 +++ Modules/_ssl.c | 60 +++++++++++++++++++++++++++++++++++++++++--- 5 files changed, 120 insertions(+), 24 deletions(-) diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst index 9f3760e87aa..5232f1bdeac 100644 --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -59,6 +59,48 @@ Functions, Constants, and Exceptions .. versionchanged:: 3.3 :exc:`SSLError` used to be a subtype of :exc:`socket.error`. +.. exception:: SSLZeroReturnError + + A subclass of :exc:`SSLError` raised when trying to read or write and + the SSL connection has been closed cleanly. Note that this doesn't + mean that the underlying transport (read TCP) has been closed. + + .. versionadded:: 3.3 + +.. exception:: SSLWantReadError + + A subclass of :exc:`SSLError` raised by a :ref:`non-blocking SSL socket + ` when trying to read or write data, but more data needs + to be received on the underlying TCP transport before the request can be + fulfilled. + + .. versionadded:: 3.3 + +.. exception:: SSLWantWriteError + + A subclass of :exc:`SSLError` raised by a :ref:`non-blocking SSL socket + ` when trying to read or write data, but more data needs + to be sent on the underlying TCP transport before the request can be + fulfilled. + + .. versionadded:: 3.3 + +.. exception:: SSLSyscallError + + A subclass of :exc:`SSLError` raised when a system error was encountered + while trying to fulfill an operation on a SSL socket. Unfortunately, + there is no easy way to inspect the original errno number. + + .. versionadded:: 3.3 + +.. exception:: SSLEOFError + + A subclass of :exc:`SSLError` raised when the SSL connection has been + terminated abrupted. Generally, you shouldn't try to reuse the underlying + transport when this error is encountered. + + .. versionadded:: 3.3 + .. exception:: CertificateError Raised to signal an error with a certificate (such as mismatching diff --git a/Lib/ssl.py b/Lib/ssl.py index 39cef2c699b..76f68f0020b 100644 --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -60,7 +60,11 @@ import re import _ssl # if we can't import it, let the error propagate from _ssl import OPENSSL_VERSION_NUMBER, OPENSSL_VERSION_INFO, OPENSSL_VERSION -from _ssl import _SSLContext, SSLError +from _ssl import _SSLContext +from _ssl import ( + SSLError, SSLZeroReturnError, SSLWantReadError, SSLWantWriteError, + SSLSyscallError, SSLEOFError, + ) from _ssl import CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED from _ssl import OP_ALL, OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_TLSv1 from _ssl import RAND_status, RAND_egd, RAND_add, RAND_bytes, RAND_pseudo_bytes diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 25f3e4fe6b9..327bb8465d7 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -619,13 +619,10 @@ class NetworkedTests(unittest.TestCase): try: s.do_handshake() break - except ssl.SSLError as err: - if err.args[0] == ssl.SSL_ERROR_WANT_READ: - select.select([s], [], [], 5.0) - elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE: - select.select([], [s], [], 5.0) - else: - raise + except ssl.SSLWantReadError: + select.select([s], [], [], 5.0) + except ssl.SSLWantWriteError: + select.select([], [s], [], 5.0) # SSL established self.assertTrue(s.getpeercert()) finally: @@ -745,13 +742,10 @@ class NetworkedTests(unittest.TestCase): count += 1 s.do_handshake() break - except ssl.SSLError as err: - if err.args[0] == ssl.SSL_ERROR_WANT_READ: - select.select([s], [], []) - elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE: - select.select([], [s], []) - else: - raise + except ssl.SSLWantReadError: + select.select([s], [], []) + except ssl.SSLWantWriteError: + select.select([], [s], []) s.close() if support.verbose: sys.stdout.write("\nNeeded %d calls to do_handshake() to establish session.\n" % count) @@ -1030,12 +1024,11 @@ else: def _do_ssl_handshake(self): try: self.socket.do_handshake() - except ssl.SSLError as err: - if err.args[0] in (ssl.SSL_ERROR_WANT_READ, - ssl.SSL_ERROR_WANT_WRITE): - return - elif err.args[0] == ssl.SSL_ERROR_EOF: - return self.handle_close() + except (ssl.SSLWantReadError, ssl.SSLWantWriteError): + return + except ssl.SSLEOFError: + return self.handle_close() + except ssl.SSLError: raise except socket.error as err: if err.args[0] == errno.ECONNABORTED: diff --git a/Misc/NEWS b/Misc/NEWS index ca82490c75f..4b6b828baa9 100644 --- a/Misc/NEWS +++ b/Misc/NEWS @@ -341,6 +341,9 @@ Core and Builtins Library ------- +- Issue #11183: Add finer-grained exceptions to the ssl module, so that + you don't have to inspect the exception's attributes in the common case. + - Issue #13216: Add cp65001 codec, the Windows UTF-8 (CP_UTF8). - Issue #13226: Add RTLD_xxx constants to the os module. These constants can be diff --git a/Modules/_ssl.c b/Modules/_ssl.c index 2998605476d..dcde4ff2afe 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -99,6 +99,11 @@ static PySocketModule_APIObject PySocketModule; /* SSL error object */ static PyObject *PySSLErrorObject; +static PyObject *PySSLZeroReturnErrorObject; +static PyObject *PySSLWantReadErrorObject; +static PyObject *PySSLWantWriteErrorObject; +static PyObject *PySSLSyscallErrorObject; +static PyObject *PySSLEOFErrorObject; #ifdef WITH_THREAD @@ -191,6 +196,7 @@ static PyObject * PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) { PyObject *v; + PyObject *type = PySSLErrorObject; char buf[2048]; char *errstr; int err; @@ -203,15 +209,18 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) switch (err) { case SSL_ERROR_ZERO_RETURN: - errstr = "TLS/SSL connection has been closed"; + errstr = "TLS/SSL connection has been closed (EOF)"; + type = PySSLZeroReturnErrorObject; p = PY_SSL_ERROR_ZERO_RETURN; break; case SSL_ERROR_WANT_READ: errstr = "The operation did not complete (read)"; + type = PySSLWantReadErrorObject; p = PY_SSL_ERROR_WANT_READ; break; case SSL_ERROR_WANT_WRITE: p = PY_SSL_ERROR_WANT_WRITE; + type = PySSLWantWriteErrorObject; errstr = "The operation did not complete (write)"; break; case SSL_ERROR_WANT_X509_LOOKUP: @@ -230,6 +239,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) = (PySocketSockObject *) PyWeakref_GetObject(obj->Socket); if (ret == 0 || (((PyObject *)s) == Py_None)) { p = PY_SSL_ERROR_EOF; + type = PySSLEOFErrorObject; errstr = "EOF occurred in violation of protocol"; } else if (ret == -1) { /* underlying BIO reported an I/O error */ @@ -240,6 +250,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) return v; } else { /* possible? */ p = PY_SSL_ERROR_SYSCALL; + type = PySSLSyscallErrorObject; errstr = "Some I/O error occurred"; } } else { @@ -272,7 +283,7 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno) ERR_clear_error(); v = Py_BuildValue("(is)", p, buf); if (v != NULL) { - PyErr_SetObject(PySSLErrorObject, v); + PyErr_SetObject(type, v); Py_DECREF(v); } return NULL; @@ -2300,6 +2311,23 @@ parse_openssl_version(unsigned long libver, PyDoc_STRVAR(SSLError_doc, "An error occurred in the SSL implementation."); +PyDoc_STRVAR(SSLZeroReturnError_doc, +"SSL/TLS session closed cleanly."); + +PyDoc_STRVAR(SSLWantReadError_doc, +"Non-blocking SSL socket needs to read more data\n" +"before the requested operation can be completed."); + +PyDoc_STRVAR(SSLWantWriteError_doc, +"Non-blocking SSL socket needs to write more data\n" +"before the requested operation can be completed."); + +PyDoc_STRVAR(SSLSyscallError_doc, +"System error when attempting SSL operation."); + +PyDoc_STRVAR(SSLEOFError_doc, +"SSL/TLS connection terminated abruptly."); + PyMODINIT_FUNC PyInit__ssl(void) @@ -2343,7 +2371,33 @@ PyInit__ssl(void) NULL); if (PySSLErrorObject == NULL) return NULL; - if (PyDict_SetItemString(d, "SSLError", PySSLErrorObject) != 0) + PySSLZeroReturnErrorObject = PyErr_NewExceptionWithDoc( + "ssl.SSLZeroReturnError", SSLZeroReturnError_doc, + PySSLErrorObject, NULL); + PySSLWantReadErrorObject = PyErr_NewExceptionWithDoc( + "ssl.SSLWantReadError", SSLWantReadError_doc, + PySSLErrorObject, NULL); + PySSLWantWriteErrorObject = PyErr_NewExceptionWithDoc( + "ssl.SSLWantWriteError", SSLWantWriteError_doc, + PySSLErrorObject, NULL); + PySSLSyscallErrorObject = PyErr_NewExceptionWithDoc( + "ssl.SSLSyscallError", SSLSyscallError_doc, + PySSLErrorObject, NULL); + PySSLEOFErrorObject = PyErr_NewExceptionWithDoc( + "ssl.SSLEOFError", SSLEOFError_doc, + PySSLErrorObject, NULL); + if (PySSLZeroReturnErrorObject == NULL + || PySSLWantReadErrorObject == NULL + || PySSLWantWriteErrorObject == NULL + || PySSLSyscallErrorObject == NULL + || PySSLEOFErrorObject == NULL) + return NULL; + if (PyDict_SetItemString(d, "SSLError", PySSLErrorObject) != 0 + || PyDict_SetItemString(d, "SSLZeroReturnError", PySSLZeroReturnErrorObject) != 0 + || PyDict_SetItemString(d, "SSLWantReadError", PySSLWantReadErrorObject) != 0 + || PyDict_SetItemString(d, "SSLWantWriteError", PySSLWantWriteErrorObject) != 0 + || PyDict_SetItemString(d, "SSLSyscallError", PySSLSyscallErrorObject) != 0 + || PyDict_SetItemString(d, "SSLEOFError", PySSLEOFErrorObject) != 0) return NULL; if (PyDict_SetItemString(d, "_SSLContext", (PyObject *)&PySSLContext_Type) != 0)