/*
* pythonmod.c: unbound module C wrapper
*
* Copyright (c) 2009, Zdenek Vasicek (vasicek AT fit.vutbr.cz)
* Marek Vavrusa (xvavru00 AT stud.fit.vutbr.cz)
*
* This software is open source.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the organization nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/**
* \file
* Python module for unbound. Calls python script.
*/
/* ignore the varargs unused warning from SWIGs internal vararg support */
#ifdef __GNUC__
#pragma GCC diagnostic ignored "-Wunused-parameter"
#ifndef __clang__
#pragma GCC diagnostic ignored "-Wunused-but-set-variable"
#endif
#endif
/**
* Per query state for the iterator module.
*/
struct pythonmod_qstate {
/** Module per query data. */
PyObject* data;
};
/* The dict from __main__ could have remnants from a previous script
* invocation, in a multi python module setup. Usually this is fine since newer
* scripts will update their values. The obvious erroneous case is when mixing
* python scripts that make use of both 'init' and 'init_standard'. This
* results in 'init_standard' to persist on following scripts that don't use it
* (thus not replacing it). This is also problematic in case where a script
* does not define a required function but a previously loaded script did. The
* current solution is to make sure to clean offensive remnants that influence
* further parsing of the individual scripts.
*/
static void
clean_python_function_objects(PyObject* dict) {
const char* function_names[] = {
"init",
"init_standard",
"deinit",
"operate",
"inform_super"
};
size_t i;
/* Fetch the error state now before we cruch it */
/* exc val contains the error message
* exc tb contains stack traceback and other info. */
PyErr_Fetch(&exc_typ, &exc_val, &exc_tb);
PyErr_NormalizeException(&exc_typ, &exc_val, &exc_tb);
/* Import the modules we need - cStringIO and traceback */
modStringIO = PyImport_ImportModule("cStringIO");
if (modStringIO==NULL) {
/* python 1.4 and before */
modStringIO = PyImport_ImportModule("StringIO");
iomod = "StringIO";
}
if (modStringIO==NULL) {
/* python 3 */
modStringIO = PyImport_ImportModule("io");
iomod = "io";
}
if (modStringIO==NULL) {
log_err("pythonmod: cannot print exception, "
"cannot ImportModule cStringIO or StringIO or io");
goto cleanup;
}
modTB = PyImport_ImportModule("traceback");
if (modTB==NULL) {
log_err("pythonmod: cannot print exception, "
"cannot ImportModule traceback");
goto cleanup;
}
/* Now call the getvalue() method in the StringIO instance */
Py_DECREF(obFuncStringIO);
obFuncStringIO = PyObject_GetAttrString(obStringIO, "getvalue");
if (obFuncStringIO==NULL) {
log_err("pythonmod: cannot print exception, "
"cannot GetAttrString StringIO.getvalue");
goto cleanup;
}
Py_DECREF(obResult);
obResult = PyObject_CallObject(obFuncStringIO, NULL);
if (obResult==NULL) {
log_err("pythonmod: cannot print exception, "
"call StringIO.getvalue() failed");
goto cleanup;
}
/* And it should be a string all ready to go - duplicate it. */
if (!PyString_Check(obResult) && !PyUnicode_Check(obResult)) {
log_err("pythonmod: cannot print exception, "
"StringIO.getvalue() result did not String_Check"
" or Unicode_Check");
goto cleanup;
}
if(PyString_Check(obResult)) {
result = PyString_AsString(obResult);
} else {
ascstr = PyUnicode_AsASCIIString(obResult);
result = PyBytes_AsString(ascstr);
}
log_err("pythonmod: python error: %s", result);
/* clear the exception, by not restoring it */
/* Restore the exception state */
/* PyErr_Restore(exc_typ, exc_val, exc_tb); */
/* when using PyErr_Restore there is no need to Py_XDECREF for
* these 3 pointers. */
Py_XDECREF(exc_typ);
Py_XDECREF(exc_val);
Py_XDECREF(exc_tb);
}
/* we only want to unwind Python once at exit */
static void
pythonmod_atexit(void)
{
log_assert(py_mod_count == 0);
log_assert(mainthr != NULL);
PyEval_RestoreThread(mainthr);
Py_Finalize();
}
int pythonmod_init(struct module_env* env, int id)
{
int py_mod_idx = py_mod_count++;
/* Check Python file load */
/* uses python to open the file, this works on other platforms,
* eg. Windows, to open the file in the correct mode for python */
#if PY_MAJOR_VERSION < 3
PyFileObject = PyFile_FromString((char*)pe->fname, "r");
script_py = PyFile_AsFile(PyFileObject);
#else
script_py = fopen(pe->fname, "r");
#endif
if (script_py == NULL)
{
log_err("pythonmod: can't open file %s for reading", pe->fname);
goto python_init_fail;
}
pe->data = PyDict_New();
/* add the script filename to the global "mod_env" for trivial access */
fname = PyString_FromString(pe->fname);
if(PyDict_SetItemString(pe->data, "script", fname) < 0) {
log_err("pythonmod: could not add item to dictionary");
Py_XDECREF(fname);
goto python_init_fail;
}
Py_XDECREF(fname);
Py_XINCREF(pe->data); /* reference will be stolen below */
if(PyModule_AddObject(pe->module, "mod_env", pe->data) < 0) {
log_err("pythonmod: could not add mod_env object");
Py_XDECREF(pe->data); /* 2 times, here and on python_init_fail; */
/* on failure the reference is not stolen */
goto python_init_fail;
}
if (PyRun_SimpleFile(script_py, pe->fname) < 0) {
#if PY_MAJOR_VERSION <= 2 || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 9)
/* for python before 3.9 */
log_err("pythonmod: can't parse Python script %s", pe->fname);
/* print the error to logs too, run it again */
fseek(script_py, 0, SEEK_SET);
/* we don't run the file, like this, because then side-effects
* s = PyRun_File(script_py, pe->fname, Py_file_input,
* PyModule_GetDict(PyImport_AddModule("__main__")), pe->dict);
* could happen (again). Instead we parse the file again to get
* the error string in the logs, for when the daemon has stderr
* removed. SimpleFile run already printed to stderr, for then
* this is called from unbound-checkconf or unbound -dd the user
* has a nice formatted error.
*/
/* ignore the NULL return of _node, it is NULL due to the parse failure
* that we are expecting */
(void)PyParser_SimpleParseFile(script_py, pe->fname, Py_file_input);
#else
/* for python 3.9 and newer */
char* fstr = NULL;
size_t flen = 0;
log_err("pythonmod: can't parse Python script %s", pe->fname);
/* print the error to logs too, run it again */
fseek(script_py, 0, SEEK_END);
flen = (size_t)ftell(script_py);
fstr = malloc(flen+1);
if(!fstr) {
log_err("malloc failure to print parse error");
/* close the file */
#if PY_MAJOR_VERSION < 3
Py_XDECREF(PyFileObject);
#else
fclose(script_py);
#endif
/* close the file */
#if PY_MAJOR_VERSION < 3
Py_XDECREF(PyFileObject);
#else
fclose(script_py);
#endif
goto python_init_fail;
}
fstr[flen] = 0;
/* we compile the string, but do not run it, to stop side-effects */
/* ignore the NULL return of _node, it is NULL due to the parse failure
* that we are expecting */
(void)Py_CompileString(fstr, pe->fname, Py_file_input);
#endif
log_py_err();
/* close the file */
#if PY_MAJOR_VERSION < 3
Py_XDECREF(PyFileObject);
#else
fclose(script_py);
#endif
#if PY_MAJOR_VERSION <= 2 || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 9)
/* no cleanup needed for python before 3.9 */
#else
/* cleanup for python 3.9 and newer */
free(fstr);
#endif
goto python_init_fail;
}
/* close the file */
#if PY_MAJOR_VERSION < 3
Py_XDECREF(PyFileObject);
#else
fclose(script_py);
#endif
if ((pe->func_init = PyDict_GetItemString(pe->dict, "init_standard")) == NULL)
{
init_standard = 0;
if ((pe->func_init = PyDict_GetItemString(pe->dict, "init")) == NULL)
{
log_err("pythonmod: function init is missing in %s", pe->fname);
goto python_init_fail;
}
}
Py_XINCREF(pe->func_init);
if ((pe->func_deinit = PyDict_GetItemString(pe->dict, "deinit")) == NULL)
{
log_err("pythonmod: function deinit is missing in %s", pe->fname);
goto python_init_fail;
}
Py_XINCREF(pe->func_deinit);
if ((pe->func_operate = PyDict_GetItemString(pe->dict, "operate")) == NULL)
{
log_err("pythonmod: function operate is missing in %s", pe->fname);
goto python_init_fail;
}
Py_XINCREF(pe->func_operate);
if ((pe->func_inform = PyDict_GetItemString(pe->dict, "inform_super")) == NULL)
{
log_err("pythonmod: function inform_super is missing in %s", pe->fname);
goto python_init_fail;
}
Py_XINCREF(pe->func_inform);
if (init_standard)
{
py_init_arg = SWIG_NewPointerObj((void*) env, SWIGTYPE_p_module_env, 0);
}
else
{
py_init_arg = SWIG_NewPointerObj((void*) env->cfg,
SWIGTYPE_p_config_file, 0);
}
res = PyObject_CallFunction(pe->func_init, "iO", id, py_init_arg);
if (PyErr_Occurred())
{
log_err("pythonmod: Exception occurred in function init");
log_py_err();
goto python_init_fail;
}
/* Deinit module */
res = PyObject_CallFunction(pe->func_deinit, "i", id);
if (PyErr_Occurred()) {
log_err("pythonmod: Exception occurred in function deinit");
log_py_err();
}
/* Free result if any */
Py_XDECREF(res);
/* Free shared data if any */
Py_XDECREF(pe->module);
Py_XDECREF(pe->dict);
Py_XDECREF(pe->data);
Py_XDECREF(pe->func_init);
Py_XDECREF(pe->func_deinit);
Py_XDECREF(pe->func_inform);
Py_XDECREF(pe->func_operate);
PyGILState_Release(gil);
py_mod_count--;
}
pe->fname = NULL;
free(pe);
/* iterate over all possible callback types and clean up each in turn */
for (cbtype = 0; cbtype < inplace_cb_types_total; cbtype++)
inplace_cb_delete(env, cbtype, id);
/* Module is deallocated in Python */
env->modinfo[id] = NULL;
}
void pythonmod_inform_super(struct module_qstate* qstate, int id, struct module_qstate* super)
{
struct pythonmod_env* pe = (struct pythonmod_env*)qstate->env->modinfo[id];
struct pythonmod_qstate* pq = (struct pythonmod_qstate*)qstate->minfo[id];
PyObject* py_qstate, *py_sqstate, *res;
PyGILState_STATE gil = PyGILState_Ensure();
log_query_info(VERB_ALGO, "pythonmod: inform_super, sub is", &qstate->qinfo);
log_query_info(VERB_ALGO, "super is", &super->qinfo);
res = PyObject_CallFunction(pe->func_inform, "iOOO", id, py_qstate,
py_sqstate, pq->data);
if (PyErr_Occurred())
{
log_err("pythonmod: Exception occurred in function inform_super");
log_py_err();
qstate->ext_state[id] = module_error;
}
else if ((res == NULL) || (!PyObject_IsTrue(res)))
{
log_err("pythonmod: python returned bad code in inform_super");
qstate->ext_state[id] = module_error;
}