commit 8336517fc520c5cf20a13ec8a3ece473a9b728c6 Author: brettlangdon Date: Sat Feb 1 19:39:56 2020 -0500 initial prototype diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd9c279 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.so +*.py[co] +build/ +ext/ +dist/ +*.egg-info +__pycache__ diff --git a/README.md b/README.md new file mode 100644 index 0000000..3870882 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +sysaudit +======== + +Backport module of [sys.audit](https://docs.python.org/3.8/library/sys.html#sys.audit) +and [sys.addaudithook](https://docs.python.org/3.8/library/sys.html#sys.addaudithook) +from Python 3.8. + +This module provides the audit hooking mechanisms and some helpers to help +library developers usage of `sys.audit`. + +**Note:** This module does _not_ backport any of the built-in +[audit events](https://docs.python.org/3.8/library/audit_events.html#audit-events). diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2256f75 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup +from distutils.core import Extension + +setup( + name="sysaudit", + version="0.1.0", + description="Backport module for sys.audit and sys.addaudithook from Python 3.8", + author="Brett Langdon", + author_email="me@brett.is", + url="https://github.com/brettlangdon/sysaudit", + ext_modules=[ + Extension( + "sysaudit.csysaudit", sources=["sysaudit/_csysaudit.c"], optional=True + ), + ], + packages=["sysaudit"], +) diff --git a/sysaudit/__init__.py b/sysaudit/__init__.py new file mode 100644 index 0000000..b5d0b25 --- /dev/null +++ b/sysaudit/__init__.py @@ -0,0 +1,4 @@ +__all__ = ["audit", "addaudithook", "extras"] + +from .audit import audit, addaudithook +from . import extras diff --git a/sysaudit/_csysaudit.c b/sysaudit/_csysaudit.c new file mode 100644 index 0000000..b83823f --- /dev/null +++ b/sysaudit/_csysaudit.c @@ -0,0 +1,118 @@ +#include "Python.h" + +struct CsysauditState { + PyObject* hooks; +}; + +#define csysaudit_state(o) ((struct CsysauditState*)PyModule_GetState(o)) + + +static PyObject* csysaudit_audit(PyObject* self, PyObject* const *args, Py_ssize_t argc) { + if (argc == 0) { + PyErr_SetString(PyExc_TypeError, "audit() missing 1 required positional argument: 'event'"); + return NULL; + } + + PyObject* return_value = NULL; + PyObject* event_name = NULL; + PyTupleObject* event_args = NULL; + PyObject* hooks = NULL; + PyObject* hook = NULL; + + event_name = args[0]; + if (!event_name) { + PyErr_SetString(PyExc_TypeError, "expected str for argument 'event'"); + goto exit; + } + + if (!PyUnicode_Check(event_name)) { + PyErr_Format(PyExc_TypeError, "expected str for argument 'event', not %.200s", + Py_TYPE(event_name)->tp_name); + goto exit; + } + + event_args = (PyTupleObject*)PyTuple_New(argc - 1); + if (!event_args) { + goto exit; + } + + PyObject** dst = event_args->ob_item; + for (Py_ssize_t i = 1; i < argc; i++) { + PyObject* item = args[i]; + Py_INCREF(item); + dst[i - 1] = item; + } + + hooks = PyObject_GetIter(csysaudit_state(self)->hooks); + if (!hooks) { + goto exit; + } + + while ((hook = PyIter_Next(hooks)) != NULL) { + PyObject *o; + o = PyObject_CallFunctionObjArgs(hook, event_name, event_args, NULL); + if (!o) { + break; + } + Py_DECREF(o); + Py_CLEAR(hook); + } + + Py_INCREF(Py_None); + return_value = Py_None; + + exit: + Py_XDECREF(hook); + Py_XDECREF(hooks); + Py_XDECREF(event_args); + + return return_value; +}; + + +static PyObject* csysaudit_addaudithook(PyObject* self, PyObject* const *args, Py_ssize_t nargs, PyObject* kwnames) { + PyObject* hook; + + if (nargs == 0) { + PyErr_SetString(PyExc_TypeError, "addaudithook() missing 1 required positional argument: 'hook'"); + return NULL; + } + + hook = args[0]; + + if (PyList_Append(csysaudit_state(self)->hooks, hook) < 0) { + return NULL; + } + + Py_RETURN_NONE; +}; + +static PyMethodDef csysaudit_methods[] = { + {"audit", (PyCFunction)(void(*)(void))csysaudit_audit, METH_FASTCALL, PyDoc_STR("") }, + {"addaudithook", (PyCFunction)(void(*)(void))csysaudit_addaudithook, METH_FASTCALL, PyDoc_STR("") }, + { NULL, NULL } +}; + +PyDoc_VAR(csysaudit_doc) = PyDoc_STR(""); + +static struct PyModuleDef csysaudit = { + PyModuleDef_HEAD_INIT, + "sysaudit.csysaudit", + csysaudit_doc, + sizeof(struct CsysauditState), + csysaudit_methods, + NULL, + NULL, + NULL, + NULL +}; + + +PyObject* PyInit_csysaudit() { + PyObject* res = PyModule_Create(&csysaudit); + if (!res) return NULL; + + csysaudit_state(res)->hooks = PyList_New(0); + + return res; +}; diff --git a/sysaudit/audit.py b/sysaudit/audit.py new file mode 100644 index 0000000..af3e0a9 --- /dev/null +++ b/sysaudit/audit.py @@ -0,0 +1,39 @@ +__all__ = ["audit", "addaudithook"] + +import sys + +# Python 3.8+ +# DEV: We could check `sys.version_info >= (3, 8)`, but if auditing ever gets +# back ported we want to take advantage of that +if hasattr(sys, "audit") and hasattr(sys, "addaudithook"): + audit = sys.audit + addaudithook = sys.addaudithook +else: + try: + from .csysaudit import audit, addaudithook + except ImportError: + _hooks = list() + + def audit(event, *args): + global _hooks + # Grab a copy of hooks so we don't need to lock here + hooks = _hooks.copy() + for hook in hooks: + hook(event, args) + + def addaudithook(callback): + global _hooks + + # https://docs.python.org/3.8/library/sys.html#sys.addaudithook + # Raise an auditing event `sys.addaudithook` with no arguments. + # If any existing hooks raise an exception derived from RuntimeError, + # the new hook will not be added and the exception suppressed. + # As a result, callers cannot assume that their hook has been added + # unless they control all existing hooks. + try: + audit("sys.addaudithook") + except RuntimeError: + return + + if callback not in _hooks: + _hooks.append(callback) diff --git a/sysaudit/extras.py b/sysaudit/extras.py new file mode 100644 index 0000000..f209388 --- /dev/null +++ b/sysaudit/extras.py @@ -0,0 +1,94 @@ +__all__ = ["audithook", "auditfunc", "auditmanager", "auditwsgi"] + +import contextlib +import functools +from .audit import audit, addaudithook + + +def audithook(*events): + def dec(func): + def hook(event, args): + if event in events: + func(*args) + + addaudithook(hook) + return func + + return dec + + +def auditfunc(event_prefix): + started_event = "{}.started".format(event_prefix) + finished_event = "{}.finished".format(event_prefix) + + def dec(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + ret = None + exc = None + try: + audit(started_event, args, kwargs) + + ret = func(*args, **kwargs) + return ret + except Exception as e: + exc = e + raise + finally: + audit(finished_event, ret, exc) + + return wrapper + + return dec + + +@contextlib.contextmanager +def auditmanager(event_prefix, *args): + started_event = "{}.started".format(event_prefix) + finished_event = "{}.finished".format(event_prefix) + + audit(started_event, *args) + exc = None + try: + yield + except Exception as e: + exc = e + raise + finally: + audit(finished_event, exc) + + +def auditwsgi(app, event_prefix="wsgi"): + request_event = "{}.request.started".format(event_prefix) + finished_event = "{}.request.finished".format(event_prefix) + start_response_started_event = "{}.start_response.started".format(event_prefix) + start_response_finished_event = "{}.start_response.finished".format(event_prefix) + + def wsgi(environ, start_response): + audit(request_event, environ, start_response) + + def audit_start_response(status, headers): + audit(start_response_started_event, status, headers) + ret = None + exc = None + try: + ret = start_response(status, headers) + return ret + except Exception as e: + exc = e + raise + finally: + audit(start_response_finished_event, ret, exc) + + ret = None + exc = None + try: + ret = app(environ, audit_start_response) + return ret + except Exception as e: + exc = e + raise + finally: + audit(finished_event, ret, exc) + + return wsgi