[Python-checkins] cpython: Issue #14837: SSL errors now have `library` and `reason` attributes describing

antoine.pitrou python-checkins at python.org
Fri Jun 22 21:17:08 CEST 2012


http://hg.python.org/cpython/rev/96513d71e650
changeset:   77578:96513d71e650
parent:      77574:a7237f157625
user:        Antoine Pitrou <solipsis at pitrou.net>
date:        Fri Jun 22 21:11:52 2012 +0200
summary:
  Issue #14837: SSL errors now have `library` and `reason` attributes describing precisely what happened and in which OpenSSL submodule.
The str() of a SSLError is also enhanced accordingly.

NOTE: this commit creates a reference leak.  The leak seems tied to the
use of PyType_FromSpec() to create the SSLError type.  The leak is on the
type object when it is instantiated:

>>> e = ssl.SSLError()
>>> sys.getrefcount(ssl.SSLError)
35
>>> e = ssl.SSLError()
>>> sys.getrefcount(ssl.SSLError)
36
>>> e = ssl.SSLError()
>>> sys.getrefcount(ssl.SSLError)
37

files:
  Doc/library/ssl.rst  |   16 +
  Lib/test/test_ssl.py |   45 +++++-
  Misc/NEWS            |    4 +
  Modules/_ssl.c       |  262 +++++++++++++++++++++++-------
  4 files changed, 262 insertions(+), 65 deletions(-)


diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst
--- a/Doc/library/ssl.rst
+++ b/Doc/library/ssl.rst
@@ -59,6 +59,22 @@
    .. versionchanged:: 3.3
       :exc:`SSLError` used to be a subtype of :exc:`socket.error`.
 
+   .. attribute:: library
+
+      A string mnemonic designating the OpenSSL submodule in which the error
+      occurred, such as ``SSL``, ``PEM`` or ``X509``.  The range of possible
+      values depends on the OpenSSL version.
+
+      .. versionadded:: 3.3
+
+   .. attribute:: reason
+
+      A string mnemonic designating the reason this error occurred, for
+      example ``CERTIFICATE_VERIFY_FAILED``.  The range of possible
+      values depends on the OpenSSL version.
+
+      .. versionadded:: 3.3
+
 .. exception:: SSLZeroReturnError
 
    A subclass of :exc:`SSLError` raised when trying to read or write and
diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -552,7 +552,7 @@
         with self.assertRaises(FileNotFoundError) as cm:
             ctx.load_dh_params(WRONGCERT)
         self.assertEqual(cm.exception.errno, errno.ENOENT)
-        with self.assertRaisesRegex(ssl.SSLError, "PEM routines"):
+        with self.assertRaises(ssl.SSLError) as cm:
             ctx.load_dh_params(CERTFILE)
 
     @skip_if_broken_ubuntu_ssl
@@ -590,6 +590,47 @@
         self.assertRaises(ValueError, ctx.set_ecdh_curve, b"foo")
 
 
+class SSLErrorTests(unittest.TestCase):
+
+    def test_str(self):
+        # The str() of a SSLError doesn't include the errno
+        e = ssl.SSLError(1, "foo")
+        self.assertEqual(str(e), "foo")
+        self.assertEqual(e.errno, 1)
+        # Same for a subclass
+        e = ssl.SSLZeroReturnError(1, "foo")
+        self.assertEqual(str(e), "foo")
+        self.assertEqual(e.errno, 1)
+
+    def test_lib_reason(self):
+        # Test the library and reason attributes
+        ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
+        with self.assertRaises(ssl.SSLError) as cm:
+            ctx.load_dh_params(CERTFILE)
+        self.assertEqual(cm.exception.library, 'PEM')
+        self.assertEqual(cm.exception.reason, 'NO_START_LINE')
+        s = str(cm.exception)
+        self.assertTrue(s.startswith("[PEM: NO_START_LINE] no start line"), s)
+
+    def test_subclass(self):
+        # Check that the appropriate SSLError subclass is raised
+        # (this only tests one of them)
+        ctx = ssl.SSLContext(ssl.PROTOCOL_TLSv1)
+        with socket.socket() as s:
+            s.bind(("127.0.0.1", 0))
+            s.listen(5)
+            with socket.socket() as c:
+                c.connect(s.getsockname())
+                c.setblocking(False)
+                c = ctx.wrap_socket(c, False, do_handshake_on_connect=False)
+                with self.assertRaises(ssl.SSLWantReadError) as cm:
+                    c.do_handshake()
+                s = str(cm.exception)
+                self.assertTrue(s.startswith("The operation did not complete (read)"), s)
+                # For compatibility
+                self.assertEqual(cm.exception.errno, ssl.SSL_ERROR_WANT_READ)
+
+
 class NetworkedTests(unittest.TestCase):
 
     def test_connect(self):
@@ -1931,7 +1972,7 @@
         if not os.path.exists(filename):
             raise support.TestFailed("Can't read certificate file %r" % filename)
 
-    tests = [ContextTests, BasicSocketTests]
+    tests = [ContextTests, BasicSocketTests, SSLErrorTests]
 
     if support.is_resource_enabled('network'):
         tests.append(NetworkedTests)
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -40,6 +40,10 @@
 Library
 -------
 
+- Issue #14837: SSL errors now have ``library`` and ``reason`` attributes
+  describing precisely what happened and in which OpenSSL submodule.  The
+  str() of a SSLError is also enhanced accordingly.
+
 - Issue #9527: datetime.astimezone() method will now supply a class
   timezone instance corresponding to the system local timezone when
   called with no arguments.
diff --git a/Modules/_ssl.c b/Modules/_ssl.c
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -76,6 +76,16 @@
     PY_SSL_VERSION_TLS1
 };
 
+struct py_ssl_error_code {
+    const char *mnemonic;
+    int library, reason;
+};
+
+struct py_ssl_library_code {
+    const char *library;
+    int code;
+};
+
 /* Include symbols from _socket module */
 #include "socketmodule.h"
 
@@ -97,6 +107,9 @@
 #include "openssl/err.h"
 #include "openssl/rand.h"
 
+/* Include generated data (error codes) */
+#include "_ssl_data.h"
+
 /* SSL error object */
 static PyObject *PySSLErrorObject;
 static PyObject *PySSLZeroReturnErrorObject;
@@ -105,6 +118,11 @@
 static PyObject *PySSLSyscallErrorObject;
 static PyObject *PySSLEOFErrorObject;
 
+/* Error mappings */
+static PyObject *err_codes_to_names;
+static PyObject *err_names_to_codes;
+static PyObject *lib_codes_to_names;
+
 #ifdef WITH_THREAD
 
 /* serves as a flag to see whether we've initialized the SSL thread support. */
@@ -202,22 +220,134 @@
 #define ERRSTR1(x,y,z) (x ":" y ": " z)
 #define ERRSTR(x) ERRSTR1("_ssl.c", STRINGIFY2(__LINE__), x)
 
-/* XXX It might be helpful to augment the error message generated
-   below with the name of the SSL function that generated the error.
-   I expect it's obvious most of the time.
-*/
+
+/*
+ * SSL errors.
+ */
+
+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.");
+
+static PyObject *
+SSLError_str(PyOSErrorObject *self)
+{
+    if (self->strerror != NULL && PyUnicode_Check(self->strerror)) {
+        Py_INCREF(self->strerror);
+        return self->strerror;
+    }
+    else
+        return PyObject_Str(self->args);
+}
+
+static PyType_Slot sslerror_type_slots[] = {
+    {Py_tp_base, NULL},  /* Filled out in module init as it's not a constant */
+    {Py_tp_doc, SSLError_doc},
+    {Py_tp_str, SSLError_str},
+    {0, 0},
+};
+
+static PyType_Spec sslerror_type_spec = {
+    "ssl.SSLError",
+    sizeof(PyOSErrorObject),
+    0,
+    Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+    sslerror_type_slots
+};
+
+static void
+fill_and_set_sslerror(PyObject *type, int ssl_errno, const char *errstr,
+                      int lineno, unsigned long errcode)
+{
+    PyObject *err_value = NULL, *reason_obj = NULL, *lib_obj = NULL;
+    PyObject *init_value, *msg, *key;
+    _Py_IDENTIFIER(reason);
+    _Py_IDENTIFIER(library);
+
+    if (errcode != 0) {
+        int lib, reason;
+
+        lib = ERR_GET_LIB(errcode);
+        reason = ERR_GET_REASON(errcode);
+        key = Py_BuildValue("ii", lib, reason);
+        if (key == NULL)
+            goto fail;
+        reason_obj = PyDict_GetItem(err_codes_to_names, key);
+        Py_DECREF(key);
+        if (reason_obj == NULL) {
+            /* XXX if reason < 100, it might reflect a library number (!!) */
+            PyErr_Clear();
+        }
+        key = PyLong_FromLong(lib);
+        if (key == NULL)
+            goto fail;
+        lib_obj = PyDict_GetItem(lib_codes_to_names, key);
+        Py_DECREF(key);
+        if (lib_obj == NULL) {
+            PyErr_Clear();
+        }
+        if (errstr == NULL)
+            errstr = ERR_reason_error_string(errcode);
+    }
+    if (errstr == NULL)
+        errstr = "unknown error";
+
+    if (reason_obj && lib_obj)
+        msg = PyUnicode_FromFormat("[%S: %S] %s (_ssl.c:%d)",
+                                   lib_obj, reason_obj, errstr, lineno);
+    else if (lib_obj)
+        msg = PyUnicode_FromFormat("[%S] %s (_ssl.c:%d)",
+                                   lib_obj, errstr, lineno);
+    else
+        msg = PyUnicode_FromFormat("%s (_ssl.c:%d)", errstr, lineno);
+
+    if (msg == NULL)
+        goto fail;
+    init_value = Py_BuildValue("iN", ssl_errno, msg);
+    err_value = PyObject_CallObject(type, init_value);
+    Py_DECREF(init_value);
+    if (err_value == NULL)
+        goto fail;
+    if (reason_obj == NULL)
+        reason_obj = Py_None;
+    if (_PyObject_SetAttrId(err_value, &PyId_reason, reason_obj))
+        goto fail;
+    if (lib_obj == NULL)
+        lib_obj = Py_None;
+    if (_PyObject_SetAttrId(err_value, &PyId_library, lib_obj))
+        goto fail;
+    PyErr_SetObject(type, err_value);
+fail:
+    Py_XDECREF(err_value);
+}
 
 static PyObject *
 PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno)
 {
-    PyObject *v;
     PyObject *type = PySSLErrorObject;
-    char buf[2048];
-    char *errstr;
+    char *errstr = NULL;
     int err;
     enum py_ssl_error p = PY_SSL_ERROR_NONE;
+    unsigned long e = 0;
 
     assert(ret <= 0);
+    e = ERR_peek_last_error();
 
     if (obj->ssl != NULL) {
         err = SSL_get_error(obj->ssl, ret);
@@ -248,7 +378,6 @@
             break;
         case SSL_ERROR_SYSCALL:
         {
-            unsigned long e = ERR_get_error();
             if (e == 0) {
                 PySocketSockObject *s
                   = (PySocketSockObject *) PyWeakref_GetObject(obj->Socket);
@@ -260,9 +389,9 @@
                     /* underlying BIO reported an I/O error */
                     Py_INCREF(s);
                     ERR_clear_error();
-                    v = s->errorhandler();
+                    s->errorhandler();
                     Py_DECREF(s);
-                    return v;
+                    return NULL;
                 } else { /* possible? */
                     p = PY_SSL_ERROR_SYSCALL;
                     type = PySSLSyscallErrorObject;
@@ -270,60 +399,43 @@
                 }
             } else {
                 p = PY_SSL_ERROR_SYSCALL;
-                /* XXX Protected by global interpreter lock */
-                errstr = ERR_error_string(e, NULL);
             }
             break;
         }
         case SSL_ERROR_SSL:
         {
-            unsigned long e = ERR_get_error();
             p = PY_SSL_ERROR_SSL;
-            if (e != 0)
-                /* XXX Protected by global interpreter lock */
-                errstr = ERR_error_string(e, NULL);
-            else {              /* possible? */
+            if (e == 0)
+                /* possible? */
                 errstr = "A failure in the SSL library occurred";
-            }
             break;
         }
         default:
             p = PY_SSL_ERROR_INVALID_ERROR_CODE;
             errstr = "Invalid error code";
         }
-    } else {
-        errstr = ERR_error_string(ERR_peek_last_error(), NULL);
     }
-    PyOS_snprintf(buf, sizeof(buf), "_ssl.c:%d: %s", lineno, errstr);
+    fill_and_set_sslerror(type, p, errstr, lineno, e);
     ERR_clear_error();
-    v = Py_BuildValue("(is)", p, buf);
-    if (v != NULL) {
-        PyErr_SetObject(type, v);
-        Py_DECREF(v);
-    }
     return NULL;
 }
 
 static PyObject *
 _setSSLError (char *errstr, int errcode, char *filename, int lineno) {
 
-    char buf[2048];
-    PyObject *v;
-
-    if (errstr == NULL) {
+    if (errstr == NULL)
         errcode = ERR_peek_last_error();
-        errstr = ERR_error_string(errcode, NULL);
-    }
-    PyOS_snprintf(buf, sizeof(buf), "_ssl.c:%d: %s", lineno, errstr);
+    else
+        errcode = 0;
+    fill_and_set_sslerror(PySSLErrorObject, errcode, errstr, lineno, errcode);
     ERR_clear_error();
-    v = Py_BuildValue("(is)", errcode, buf);
-    if (v != NULL) {
-        PyErr_SetObject(PySSLErrorObject, v);
-        Py_DECREF(v);
-    }
     return NULL;
 }
 
+/*
+ * SSL objects
+ */
+
 static PySSLSocket *
 newPySSLSocket(SSL_CTX *ctx, PySocketSockObject *sock,
                enum py_ssl_server_or_client socket_type,
@@ -2520,27 +2632,6 @@
     *major = libver & 0xFF;
 }
 
-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)
 {
@@ -2548,6 +2639,8 @@
     unsigned long libver;
     unsigned int major, minor, fix, patch, status;
     PySocketModule_APIObject *socket_api;
+    struct py_ssl_error_code *errcode;
+    struct py_ssl_library_code *libcode;
 
     if (PyType_Ready(&PySSLContext_Type) < 0)
         return NULL;
@@ -2577,12 +2670,11 @@
     OpenSSL_add_all_algorithms();
 
     /* Add symbols to module dict */
-    PySSLErrorObject = PyErr_NewExceptionWithDoc("ssl.SSLError",
-                                                 SSLError_doc,
-                                                 PyExc_OSError,
-                                                 NULL);
+    sslerror_type_slots[0].pfunc = PyExc_OSError;
+    PySSLErrorObject = PyType_FromSpec(&sslerror_type_spec);
     if (PySSLErrorObject == NULL)
         return NULL;
+
     PySSLZeroReturnErrorObject = PyErr_NewExceptionWithDoc(
         "ssl.SSLZeroReturnError", SSLZeroReturnError_doc,
         PySSLErrorObject, NULL);
@@ -2705,6 +2797,50 @@
     Py_INCREF(r);
     PyModule_AddObject(m, "HAS_NPN", r);
 
+    /* Mappings for error codes */
+    err_codes_to_names = PyDict_New();
+    err_names_to_codes = PyDict_New();
+    if (err_codes_to_names == NULL || err_names_to_codes == NULL)
+        return NULL;
+    errcode = error_codes;
+    while (errcode->mnemonic != NULL) {
+        PyObject *mnemo, *key;
+        mnemo = PyUnicode_FromString(errcode->mnemonic);
+        key = Py_BuildValue("ii", errcode->library, errcode->reason);
+        if (mnemo == NULL || key == NULL)
+            return NULL;
+        if (PyDict_SetItem(err_codes_to_names, key, mnemo))
+            return NULL;
+        if (PyDict_SetItem(err_names_to_codes, mnemo, key))
+            return NULL;
+        Py_DECREF(key);
+        Py_DECREF(mnemo);
+        errcode++;
+    }
+    if (PyModule_AddObject(m, "err_codes_to_names", err_codes_to_names))
+        return NULL;
+    if (PyModule_AddObject(m, "err_names_to_codes", err_names_to_codes))
+        return NULL;
+
+    lib_codes_to_names = PyDict_New();
+    if (lib_codes_to_names == NULL)
+        return NULL;
+    libcode = library_codes;
+    while (libcode->library != NULL) {
+        PyObject *mnemo, *key;
+        key = PyLong_FromLong(libcode->code);
+        mnemo = PyUnicode_FromString(libcode->library);
+        if (key == NULL || mnemo == NULL)
+            return NULL;
+        if (PyDict_SetItem(lib_codes_to_names, key, mnemo))
+            return NULL;
+        Py_DECREF(key);
+        Py_DECREF(mnemo);
+        libcode++;
+    }
+    if (PyModule_AddObject(m, "lib_codes_to_names", lib_codes_to_names))
+        return NULL;
+    
     /* OpenSSL version */
     /* SSLeay() gives us the version of the library linked against,
        which could be different from the headers version.

-- 
Repository URL: http://hg.python.org/cpython


More information about the Python-checkins mailing list