summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAntoine Pitrou <solipsis@pitrou.net>2012-06-22 19:11:52 (GMT)
committerAntoine Pitrou <solipsis@pitrou.net>2012-06-22 19:11:52 (GMT)
commit3b36fb1f537a771829c9c7c9720dc040d3c87885 (patch)
treeaaec4c06b55dfb3681c56be22fe1112a79d3c433
parent31227ca5148436ee3e48430814c59fc31876821d (diff)
downloadcpython-3b36fb1f537a771829c9c7c9720dc040d3c87885.zip
cpython-3b36fb1f537a771829c9c7c9720dc040d3c87885.tar.gz
cpython-3b36fb1f537a771829c9c7c9720dc040d3c87885.tar.bz2
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
-rw-r--r--Doc/library/ssl.rst16
-rw-r--r--Lib/test/test_ssl.py45
-rw-r--r--Misc/NEWS4
-rw-r--r--Modules/_ssl.c262
4 files changed, 262 insertions, 65 deletions
diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst
index 8092581..e08c2b9 100644
--- a/Doc/library/ssl.rst
+++ b/Doc/library/ssl.rst
@@ -59,6 +59,22 @@ Functions, Constants, and Exceptions
.. 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
index d2839dd..51762cf 100644
--- a/Lib/test/test_ssl.py
+++ b/Lib/test/test_ssl.py
@@ -552,7 +552,7 @@ class ContextTests(unittest.TestCase):
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 @@ class ContextTests(unittest.TestCase):
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 @@ def test_main(verbose=False):
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
index af1e26c..a733354 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -40,6 +40,10 @@ Core and Builtins
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
index d755411..b5d1530 100644
--- a/Modules/_ssl.c
+++ b/Modules/_ssl.c
@@ -76,6 +76,16 @@ enum py_ssl_version {
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 @@ static PySocketModule_APIObject PySocketModule;
#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 *PySSLWantWriteErrorObject;
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 @@ typedef enum {
#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 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno)
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 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno)
/* 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 @@ PySSL_SetError(PySSLSocket *obj, int ret, char *filename, int lineno)
}
} 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 @@ parse_openssl_version(unsigned long libver,
*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 @@ PyInit__ssl(void)
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 @@ PyInit__ssl(void)
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 @@ PyInit__ssl(void)
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.