diff options
Diffstat (limited to 'src/modules/rlm_python/rlm_python.c')
-rw-r--r-- | src/modules/rlm_python/rlm_python.c | 1284 |
1 files changed, 1284 insertions, 0 deletions
diff --git a/src/modules/rlm_python/rlm_python.c b/src/modules/rlm_python/rlm_python.c new file mode 100644 index 0000000..2adba0e --- /dev/null +++ b/src/modules/rlm_python/rlm_python.c @@ -0,0 +1,1284 @@ +/* + * This program is is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or (at + * your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * @file rlm_python.c + * @brief Translates requests between the server an a python interpreter. + * + * @note Rewritten by Paul P. Komkoff Jr <i@stingr.net>. + * + * @copyright 2000,2006,2015-2016 The FreeRADIUS server project + * @copyright 2002 Miguel A.L. Paraz <mparaz@mparaz.com> + * @copyright 2002 Imperium Technology, Inc. + */ +RCSID("$Id$") + +#define LOG_PREFIX "rlm_python - " + +#include "config.h" +#include <freeradius-devel/radiusd.h> +#include <freeradius-devel/modules.h> +#include <freeradius-devel/rad_assert.h> + +#include <Python.h> +#include <frameobject.h> /* Python header not pulled in by default. */ +#include <dlfcn.h> +#ifdef HAVE_DL_ITERATE_PHDR +#include <link.h> +#endif + +#define LIBPYTHON_LINKER_NAME \ + "libpython" STRINGIFY(PY_MAJOR_VERSION) "." STRINGIFY(PY_MINOR_VERSION) LT_SHREXT + +static uint32_t python_instances = 0; +static void *python_dlhandle; + +static PyThreadState *main_interpreter; //!< Main interpreter (cext safe) +static PyObject *main_module; //!< Pthon configuration dictionary. + +/** Specifies the module.function to load for processing a section + * + */ +typedef struct python_func_def { + PyObject *module; //!< Python reference to module. + PyObject *function; //!< Python reference to function in module. + + char const *module_name; //!< String name of module. + char const *function_name; //!< String name of function in module. +} python_func_def_t; + +/** An instance of the rlm_python module + * + */ +typedef struct rlm_python_t { + char const *name; //!< Name of the module instance + PyThreadState *sub_interpreter; //!< The main interpreter/thread used for this instance. + char const *python_path; //!< Path to search for python files in. + +#if PY_VERSION_HEX > 0x03050000 + wchar_t *wide_name; //!< Special wide char encoding of radiusd name. +#endif + PyObject *module; //!< Local, interpreter specific module, containing + //!< FreeRADIUS functions. + bool cext_compat; //!< Whether or not to create sub-interpreters per module + //!< instance. + + python_func_def_t + instantiate, + authorize, + authenticate, + preacct, + accounting, + checksimul, + pre_proxy, + post_proxy, + post_auth, +#ifdef WITH_COA + recv_coa, + send_coa, +#endif + detach; + + PyObject *pythonconf_dict; //!< Configuration parameters defined in the module + //!< made available to the python script. + bool pass_all_vps; //!< Pass all VPS lists (request, reply, config, state, proxy_req, proxy_reply) + bool pass_all_vps_dict; //!< Pass all VPS lists as a dictionary rather than a tuple +} rlm_python_t; + +/** Tracks a python module inst/thread state pair + * + * Multiple instances of python create multiple interpreters and each + * thread must have a PyThreadState per interpreter, to track execution. + */ +typedef struct python_thread_state { + PyThreadState *state; //!< Module instance/thread specific state. + rlm_python_t *inst; //!< Module instance that created this thread state. +} python_thread_state_t; + +/* + * A mapping of configuration file names to internal variables. + */ +static CONF_PARSER module_config[] = { + +#define A(x) { "mod_" #x, FR_CONF_OFFSET(PW_TYPE_STRING, rlm_python_t, x.module_name), NULL }, \ + { "func_" #x, FR_CONF_OFFSET(PW_TYPE_STRING, rlm_python_t, x.function_name), NULL }, + + A(instantiate) + A(authorize) + A(authenticate) + A(preacct) + A(accounting) + A(checksimul) + A(pre_proxy) + A(post_proxy) + A(post_auth) +#ifdef WITH_COA + A(recv_coa) + A(send_coa) +#endif + A(detach) + +#undef A + + { "python_path", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_python_t, python_path), NULL }, + { "cext_compat", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_python_t, cext_compat), "yes" }, + { "pass_all_vps", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_python_t, pass_all_vps), "no" }, + { "pass_all_vps_dict", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_python_t, pass_all_vps_dict), "no" }, + + CONF_PARSER_TERMINATOR +}; + +static struct { + char const *name; + int value; +} radiusd_constants[] = { + +#define A(x) { #x, x }, + + A(L_DBG) + A(L_WARN) + A(L_AUTH) + A(L_INFO) + A(L_ERR) + A(L_PROXY) + A(L_ACCT) + A(L_DBG_WARN) + A(L_DBG_ERR) + A(L_DBG_WARN_REQ) + A(L_DBG_ERR_REQ) + A(RLM_MODULE_REJECT) + A(RLM_MODULE_FAIL) + A(RLM_MODULE_OK) + A(RLM_MODULE_HANDLED) + A(RLM_MODULE_INVALID) + A(RLM_MODULE_USERLOCK) + A(RLM_MODULE_NOTFOUND) + A(RLM_MODULE_NOOP) + A(RLM_MODULE_UPDATED) + A(RLM_MODULE_NUMCODES) + +#undef A + + { NULL, 0 }, +}; + +/* + * This allows us to initialise PyThreadState on a per thread basis + */ +fr_thread_local_setup(rbtree_t *, local_thread_state) /* macro */ + +/* + * radiusd Python functions + */ + +/** Allow radlog to be called from python + * + */ +static PyObject *mod_radlog(UNUSED PyObject *module, PyObject *args) +{ + int status; + char *msg; + + if (!PyArg_ParseTuple(args, "is", &status, &msg)) { + return NULL; + } + + radlog(status, "%s", msg); + Py_INCREF(Py_None); + + return Py_None; +} + +static PyMethodDef module_methods[] = { + { "radlog", &mod_radlog, METH_VARARGS, + "radiusd.radlog(level, msg)\n\n" \ + "Print a message using radiusd logging system. level should be one of the\n" \ + "constants L_DBG, L_AUTH, L_INFO, L_ERR, L_PROXY\n" + }, + { NULL, NULL, 0, NULL }, +}; + +/** Print out the current error + * + * Must be called with a valid thread state set + */ +static void python_error_log(void) +{ + PyObject *p_type = NULL, *p_value = NULL, *p_traceback = NULL, *p_str_1 = NULL, *p_str_2 = NULL; + + PyErr_Fetch(&p_type, &p_value, &p_traceback); + PyErr_NormalizeException(&p_type, &p_value, &p_traceback); + if (!p_type || !p_value) goto failed; + + if (((p_str_1 = PyObject_Str(p_type)) == NULL) || ((p_str_2 = PyObject_Str(p_value)) == NULL)) goto failed; + + ERROR("%s (%s)", PyString_AsString(p_str_1), PyString_AsString(p_str_2)); + + if (p_traceback != Py_None) { + PyTracebackObject *ptb = (PyTracebackObject*)p_traceback; + size_t fnum = 0; + + for (; ptb != NULL; ptb = ptb->tb_next, fnum++) { + PyFrameObject *cur_frame = ptb->tb_frame; + + ERROR("[%ld] %s:%d at %s()", + fnum, + PyString_AsString(cur_frame->f_code->co_filename), + PyFrame_GetLineNumber(cur_frame), + PyString_AsString(cur_frame->f_code->co_name) + ); + } + } + +failed: + Py_XDECREF(p_str_1); + Py_XDECREF(p_str_2); + Py_XDECREF(p_type); + Py_XDECREF(p_value); + Py_XDECREF(p_traceback); +} + +static void mod_vptuple(TALLOC_CTX *ctx, REQUEST *request, VALUE_PAIR **vps, PyObject *pValue, + char const *funcname, char const *list_name) +{ + int i; + int tuplesize; + vp_tmpl_t dst; + VALUE_PAIR *vp; + REQUEST *current = request; + + memset(&dst, 0, sizeof(dst)); + + /* + * If the Python function gave us None for the tuple, + * then just return. + */ + if (pValue == Py_None || pValue == NULL) return; + + if (!PyTuple_CheckExact(pValue)) { + ERROR("%s - non-tuple passed to %s", funcname, list_name); + return; + } + /* Get the tuple tuplesize. */ + tuplesize = PyTuple_GET_SIZE(pValue); + for (i = 0; i < tuplesize; i++) { + PyObject *pTupleElement = PyTuple_GET_ITEM(pValue, i); + PyObject *pStr1; + PyObject *pStr2; + PyObject *pOp; + int pairsize; + char const *s1; + char const *s2; + FR_TOKEN op = T_OP_EQ; + + if (!PyTuple_CheckExact(pTupleElement)) { + ERROR("%s - Tuple element %d of %s is not a tuple", funcname, i, list_name); + continue; + } + /* Check if it's a pair */ + + pairsize = PyTuple_GET_SIZE(pTupleElement); + if ((pairsize < 2) || (pairsize > 3)) { + ERROR("%s - Tuple element %d of %s is a tuple of size %d. Must be 2 or 3", + funcname, i, list_name, pairsize); + continue; + } + + pStr1 = PyTuple_GET_ITEM(pTupleElement, 0); + pStr2 = PyTuple_GET_ITEM(pTupleElement, pairsize-1); + + if ((!PyString_CheckExact(pStr1)) || (!PyString_CheckExact(pStr2))) { + ERROR("%s - Tuple element %d of %s must be as (str, str)", + funcname, i, list_name); + continue; + } + s1 = PyString_AsString(pStr1); + s2 = PyString_AsString(pStr2); + + if (pairsize == 3) { + pOp = PyTuple_GET_ITEM(pTupleElement, 1); + if (PyString_CheckExact(pOp)) { + if (!(op = fr_str2int(fr_tokens, PyString_AsString(pOp), 0))) { + ERROR("%s - Invalid operator %s:%s %s %s, falling back to '='", + funcname, list_name, s1, PyString_AsString(pOp), s2); + op = T_OP_EQ; + } + } else if (PyInt_Check(pOp)) { + op = PyInt_AsLong(pOp); + if (!fr_int2str(fr_tokens, op, NULL)) { + ERROR("%s - Invalid operator %s:%s %i %s, falling back to '='", + funcname, list_name, s1, op, s2); + op = T_OP_EQ; + } + } else { + ERROR("%s - Invalid operator type for %s:%s ? %s, using default '='", + funcname, list_name, s1, s2); + } + } + + if (tmpl_from_attr_str(&dst, s1, REQUEST_CURRENT, PAIR_LIST_REPLY, false, false) <= 0) { + ERROR("%s - Failed to find attribute %s:%s", funcname, list_name, s1); + continue; + } + + if (radius_request(¤t, dst.tmpl_request) < 0) { + ERROR("%s - Attribute name %s:%s refers to outer request but not in a tunnel, skipping...", + funcname, list_name, s1); + continue; + } + + if (!(vp = fr_pair_afrom_da(ctx, dst.tmpl_da))) { + ERROR("%s - Failed to create attribute %s:%s", funcname, list_name, s1); + continue; + } + + vp->op = op; + + /* + * @todo - use tmpl_cast_to_vp() instead ??? + */ + if (vp->da->flags.has_tag) vp->tag = dst.tmpl_tag; + + if (fr_pair_value_from_str(vp, s2, -1) < 0) { + DEBUG("%s - Failed: '%s:%s' %s '%s'", funcname, list_name, s1, + fr_int2str(fr_tokens, op, "="), s2); + } else { + DEBUG("%s - '%s:%s' %s '%s'", funcname, list_name, s1, + fr_int2str(fr_tokens, op, "="), s2); + } + + radius_pairmove(current, vps, vp, false); + } +} + + +/* + * This is the core Python function that the others wrap around. + * Pass the value-pair print strings in a tuple. + * + * FIXME: We're not checking the errors. If we have errors, what + * do we do? + */ +static int mod_populate_vptuple(PyObject *pPair, VALUE_PAIR *vp) +{ + PyObject *pStr = NULL; + char buf[1024]; + + /* Look at the fr_pair_fprint_name? */ + + if (vp->da->flags.has_tag) { + pStr = PyString_FromFormat("%s:%d", vp->da->name, vp->tag); + } else { + pStr = PyString_FromString(vp->da->name); + } + + if (!pStr) return -1; + + PyTuple_SET_ITEM(pPair, 0, pStr); + + vp_prints_value(buf, sizeof(buf), vp, '\0'); /* Python doesn't need any escaping */ + + pStr = PyString_FromString(buf); + if (pStr == NULL) return -1; + + PyTuple_SET_ITEM(pPair, 1, pStr); + + return 0; +} + +/* + * This function generates a tuple representing a given VPS and inserts it into + * the indicated position in the tuple pArgs. + * Returns false on error. + */ +static bool mod_populate_vps(PyObject* pArgs, const int pos, VALUE_PAIR *vps) +{ + PyObject *vps_tuple = NULL; + int tuplelen = 0; + int i = 0; + vp_cursor_t cursor; + VALUE_PAIR *vp; + + /* If vps is NULL, return None */ + if (vps == NULL) { + Py_INCREF(Py_None); + PyTuple_SET_ITEM(pArgs, pos, Py_None); + return true; + } + + /* + * We will pass a tuple containing (name, value) tuples + * We can safely use the Python function to build up a + * tuple, since the tuple is not used elsewhere. + * + * Determine the size of our tuple by walking through the vps. + */ + for (vp = fr_cursor_init(&cursor, &vps); vp; vp = fr_cursor_next(&cursor)) + tuplelen++; + + if ((vps_tuple = PyTuple_New(tuplelen)) == NULL) goto error; + + for (vp = fr_cursor_init(&cursor, &vps); vp; vp = fr_cursor_next(&cursor), i++) { + PyObject *pPair = NULL; + + /* The inside tuple has two only: */ + if ((pPair = PyTuple_New(2)) == NULL) goto error; + + if (mod_populate_vptuple(pPair, vp) == 0) { + /* Put the tuple inside the container */ + PyTuple_SET_ITEM(vps_tuple, i, pPair); + } else { + Py_INCREF(Py_None); + PyTuple_SET_ITEM(vps_tuple, i, Py_None); + Py_DECREF(pPair); + } + } + PyTuple_SET_ITEM(pArgs, pos, vps_tuple); + return true; + +error: + Py_XDECREF(vps_tuple); + return false; +} + +static rlm_rcode_t do_python_single(REQUEST *request, PyObject *pFunc, char const *funcname, bool pass_all_vps, bool pass_all_vps_dict) +{ + PyObject *pRet = NULL; + PyObject *pArgs = NULL; + PyObject *pDictInput = NULL; + int ret; + int i; + + /* Default return value is "OK, continue" */ + ret = RLM_MODULE_OK; + + /* + * pArgs is a 6-tuple with (Request, Reply, Config, State, Proxy-Request, Proxy-Reply) + * If some list is not available, NONE is used instead + */ + if ((pArgs = PyTuple_New(6)) == NULL) { + ret = RLM_MODULE_FAIL; + goto finish; + } + + /* If there is a request, fill in the first 4 attribute lists */ + if (request != NULL) { + if (!mod_populate_vps(pArgs, 0, request->packet->vps) || + !mod_populate_vps(pArgs, 1, request->reply->vps) || + !mod_populate_vps(pArgs, 2, request->config) || + !mod_populate_vps(pArgs, 3, request->state)) { + ret = RLM_MODULE_FAIL; + goto finish; + } + + /* fill proxy vps */ + if (request->proxy) { + if (!mod_populate_vps(pArgs, 4, request->proxy->vps)) { + ret = RLM_MODULE_FAIL; + goto finish; + } + } else { + mod_populate_vps(pArgs, 4, NULL); + } + + /* fill proxy_reply vps */ + if (request->proxy_reply) { + if (!mod_populate_vps(pArgs, 5, request->proxy_reply->vps)) { + ret = RLM_MODULE_FAIL; + goto finish; + } + } else { + mod_populate_vps(pArgs, 5, NULL); + } + + } + /* If there is no request, set all the elements to None */ + else for (i = 0; i < 6; i++) mod_populate_vps(pArgs, i, NULL); + + /* + * Call Python function. If pass_all_vps_dict is true, a dictionary with the + * appropriate "request", "reply"... keys is passed as argument to the + * module callback. + * Else, if pass_all_vps is true, a 6-tuple representing + * (Request, Reply, Config, State, Proxy-Request, Proxy-Reply) is passed. + * Otherwise, a tuple representing just the request is used. + */ + if (pass_all_vps_dict) { + pDictInput = PyDict_New(); + if (pDictInput == NULL || + PyDict_SetItemString(pDictInput, "request", PyTuple_GET_ITEM(pArgs, 0)) || + PyDict_SetItemString(pDictInput, "reply", PyTuple_GET_ITEM(pArgs, 1)) || + PyDict_SetItemString(pDictInput, "config", PyTuple_GET_ITEM(pArgs, 2)) || + PyDict_SetItemString(pDictInput, "session-state", PyTuple_GET_ITEM(pArgs, 3)) || + PyDict_SetItemString(pDictInput, "proxy-request", PyTuple_GET_ITEM(pArgs, 4)) || + PyDict_SetItemString(pDictInput, "proxy-reply", PyTuple_GET_ITEM(pArgs, 5))) { + ret = RLM_MODULE_FAIL; + goto finish; + } + pRet = PyObject_CallFunctionObjArgs(pFunc, pDictInput, NULL); + } + else if (pass_all_vps) + pRet = PyObject_CallFunctionObjArgs(pFunc, pArgs, NULL); + else + pRet = PyObject_CallFunctionObjArgs(pFunc, PyTuple_GET_ITEM(pArgs, 0), NULL); + + if (!pRet) { + ret = RLM_MODULE_FAIL; + goto finish; + } + + if (!request) { + // check return code at module instantiation time + if (PyInt_CheckExact(pRet)) ret = PyInt_AsLong(pRet); + goto finish; + } + + /* + * The function returns either: + * 1. (returnvalue, replyTuple, configTuple), where + * - returnvalue is one of the constants RLM_* + * - replyTuple and configTuple are tuples of string + * tuples of size 2 + * + * 2. the function return value alone + * + * 3. None - default return value is set + * + * xxx This code is messy! + */ + if (PyTuple_CheckExact(pRet)) { + PyObject *pTupleInt; + int tuple_size = PyTuple_GET_SIZE(pRet); + + if (tuple_size < 2 || tuple_size > 3) { + ERROR("%s - Tuple must be (return, updateDict) or (return, replyTuple, configTuple)", funcname); + ret = RLM_MODULE_FAIL; + goto finish; + } + + pTupleInt = PyTuple_GET_ITEM(pRet, 0); + if (!PyInt_CheckExact(pTupleInt)) { + ERROR("%s - First tuple element not an integer", funcname); + ret = RLM_MODULE_FAIL; + goto finish; + } + /* Now have the return value */ + ret = PyInt_AsLong(pTupleInt); + + /* process updateDict */ + if (tuple_size == 2) { + PyObject *updateDict = PyTuple_GET_ITEM(pRet, 1); + if (!PyDict_CheckExact(updateDict)) { + ERROR("%s - updateDict is not a dictionary", funcname); + ret = RLM_MODULE_FAIL; + goto finish; + } + mod_vptuple(request->reply, request, &request->reply->vps, + PyDict_GetItemString(updateDict, "reply"), funcname, "reply"); + mod_vptuple(request, request, &request->config, + PyDict_GetItemString(updateDict, "config"), funcname, "config"); + mod_vptuple(request->packet, request, &request->packet->vps, + PyDict_GetItemString(updateDict, "request"), funcname, "request"); + mod_vptuple(request->state_ctx, request, &request->state, + PyDict_GetItemString(updateDict, "session-state"), funcname, "session-state"); +#ifdef WITH_PROXY + if (request->proxy) + mod_vptuple(request->proxy, request, &request->proxy->vps, + PyDict_GetItemString(updateDict, "proxy-request"), funcname, "proxy-request"); + if (request->proxy_reply) + mod_vptuple(request->proxy_reply, request, &request->proxy_reply->vps, + PyDict_GetItemString(updateDict, "proxy-reply"), funcname, "proxy-reply"); +#endif + /* + * Update cached copies + */ + request->username = fr_pair_find_by_num(request->packet->vps, PW_USER_NAME, 0, TAG_ANY); + request->password = fr_pair_find_by_num(request->packet->vps, PW_USER_PASSWORD, 0, TAG_ANY); + if (!request->password) + request->password = fr_pair_find_by_num(request->packet->vps, PW_CHAP_PASSWORD, 0, TAG_ANY); + } + + /* process replyTuple and configTuple */ + else if (tuple_size == 3) { + /* Reply item tuple */ + mod_vptuple(request->reply, request, &request->reply->vps, + PyTuple_GET_ITEM(pRet, 1), funcname, "reply"); + /* Config item tuple */ + mod_vptuple(request, request, &request->config, + PyTuple_GET_ITEM(pRet, 2), funcname, "config"); + } + } else if (PyInt_CheckExact(pRet)) { + /* Just an integer */ + ret = PyInt_AsLong(pRet); + + } else if (pRet == Py_None) { + /* returned 'None', return value defaults to "OK, continue." */ + ret = RLM_MODULE_OK; + } else { + /* Not tuple or None */ + ERROR("%s - Function did not return a tuple or None", funcname); + ret = RLM_MODULE_FAIL; + goto finish; + } + + +finish: + Py_XDECREF(pArgs); + Py_XDECREF(pRet); + Py_XDECREF(pDictInput); + + return ret; +} + +static void python_interpreter_free(PyThreadState *interp) +{ + PyEval_AcquireLock(); + PyThreadState_Swap(interp); + Py_EndInterpreter(interp); + PyEval_ReleaseLock(); +} + +/** Destroy a thread state + * + * @param thread to destroy. + * @return 0 + */ +static int _python_thread_free(python_thread_state_t *thread) +{ + PyEval_RestoreThread(thread->state); /* Swap in our local thread state */ + PyThreadState_Clear(thread->state); + PyEval_SaveThread(); + + PyThreadState_Delete(thread->state); /* Don't need to hold lock for this */ + + return 0; +} + +/** Callback for rbtree delete walker + * + */ +static void _python_thread_entry_free(void *arg) +{ + talloc_free(arg); +} + +/** Cleanup any thread local storage on pthread_exit() + * + * @param arg The thread currently exiting. + */ +static void _python_thread_tree_free(void *arg) +{ + rad_assert(arg == local_thread_state); + + rbtree_t *tree = talloc_get_type_abort(arg, rbtree_t); + rbtree_free(tree); /* Needs to be this not talloc_free to execute delete walker */ + + local_thread_state = NULL; /* Prevent double free in unittest env */ +} + +/** Compare instance pointers + * + */ +static int _python_inst_cmp(const void *a, const void *b) +{ + python_thread_state_t const *a_p = a, *b_p = b; + + if (a_p->inst < b_p->inst) return -1; + if (a_p->inst > b_p->inst) return +1; + return 0; +} + +/** Thread safe call to a python function + * + * Will swap in thread state specific to module/thread. + */ +static rlm_rcode_t do_python(rlm_python_t *inst, REQUEST *request, PyObject *pFunc, char const *funcname) +{ + int ret; + rbtree_t *thread_tree; + python_thread_state_t *this_thread; + python_thread_state_t find; + + /* + * It's a NOOP if the function wasn't defined + */ + if (!pFunc) return RLM_MODULE_NOOP; + + /* + * Check to see if we've got a thread state tree + * If not, create one. + */ + thread_tree = fr_thread_local_init(local_thread_state, _python_thread_tree_free); + if (!thread_tree) { + thread_tree = rbtree_create(NULL, _python_inst_cmp, _python_thread_entry_free, 0); + if (!thread_tree) { + RERROR("Failed allocating thread state tree"); + return RLM_MODULE_FAIL; + } + + ret = fr_thread_local_set(local_thread_state, thread_tree); + if (ret != 0) { + talloc_free(thread_tree); + return RLM_MODULE_FAIL; + } + } + + find.inst = inst; + /* + * Find the thread state associated with this instance + * and this thread, or create a new thread state. + */ + this_thread = rbtree_finddata(thread_tree, &find); + if (!this_thread) { + PyThreadState *state; + + state = PyThreadState_New(inst->sub_interpreter->interp); + + RDEBUG3("Initialised new thread state %p", state); + if (!state) { + REDEBUG("Failed initialising local PyThreadState on first run"); + return RLM_MODULE_FAIL; + } + + this_thread = talloc(NULL, python_thread_state_t); + this_thread->inst = inst; + this_thread->state = state; + talloc_set_destructor(this_thread, _python_thread_free); + + if (!rbtree_insert(thread_tree, this_thread)) { + RERROR("Failed inserting thread state into TLS tree"); + talloc_free(this_thread); + + return RLM_MODULE_FAIL; + } + } + RDEBUG3("Using thread state %p", this_thread->state); + + PyEval_RestoreThread(this_thread->state); /* Swap in our local thread state */ + ret = do_python_single(request, pFunc, funcname, inst->pass_all_vps, inst->pass_all_vps_dict); + if (ret == RLM_MODULE_FAIL) python_error_log(); + PyEval_SaveThread(); + + return ret; +} + +#define MOD_FUNC(x) \ +static rlm_rcode_t CC_HINT(nonnull) mod_##x(void *instance, REQUEST *request) { \ + return do_python((rlm_python_t *) instance, request, ((rlm_python_t *)instance)->x.function, #x);\ +} + +MOD_FUNC(authenticate) +MOD_FUNC(authorize) +MOD_FUNC(preacct) +MOD_FUNC(accounting) +MOD_FUNC(checksimul) +MOD_FUNC(pre_proxy) +MOD_FUNC(post_proxy) +MOD_FUNC(post_auth) +#ifdef WITH_COA +MOD_FUNC(recv_coa) +MOD_FUNC(send_coa) +#endif +static void python_obj_destroy(PyObject **ob) +{ + if (*ob != NULL) { + Py_DECREF(*ob); + *ob = NULL; + } +} + +static void python_function_destroy(python_func_def_t *def) +{ + python_obj_destroy(&def->function); + python_obj_destroy(&def->module); +} + +/** Import a user module and load a function from it + * + */ +static int python_function_load(char const *name, python_func_def_t *def) +{ + if (!def->module_name && !def->function_name) return 0; /* Just not set, it's fine */ + + if (!def->module_name) { + ERROR("Once you have set the 'func_%s = %s', you should set 'mod_%s = ...' too.", + name, def->function_name, name); + return -1; + } + + if (!def->function_name) { + ERROR("Once you have set the 'mod_%s = %s', you should set 'func_%s = ...' too.", + name, def->module_name, name); + return -1; + } + + def->module = PyImport_ImportModule(def->module_name); + if (!def->module) { + ERROR("%s - Module '%s' not found", __func__, def->module_name); + + error: + python_error_log(); + ERROR("%s - Failed to import python function '%s.%s'", + __func__, def->module_name, def->function_name); + Py_XDECREF(def->function); + def->function = NULL; + Py_XDECREF(def->module); + def->module = NULL; + + return -1; + } + + def->function = PyObject_GetAttrString(def->module, def->function_name); + if (!def->function) { + ERROR("%s - Function '%s.%s' is not found", __func__, def->module_name, def->function_name); + goto error; + } + + if (!PyCallable_Check(def->function)) { + ERROR("%s - Function '%s.%s' is not callable", __func__, def->module_name, def->function_name); + goto error; + } + + return 0; +} + +/* + * Parse a configuration section, and populate a dict. + * This function is recursively called (allows to have nested dicts.) + */ +static void python_parse_config(CONF_SECTION *cs, int lvl, PyObject *dict) +{ + int indent_section = (lvl + 1) * 4; + int indent_item = (lvl + 2) * 4; + CONF_ITEM *ci = NULL; + + if (!cs || !dict) return; + + DEBUG("%*s%s {", indent_section, " ", cf_section_name1(cs)); + + while ((ci = cf_item_find_next(cs, ci))) { + /* + * This is a section. + * Create a new dict, store it in current dict, + * Then recursively call python_parse_config with this section and the new dict. + */ + if (cf_item_is_section(ci)) { + CONF_SECTION *sub_cs = cf_item_to_section(ci); + char const *key = cf_section_name1(sub_cs); /* dict key */ + PyObject *sub_dict, *pKey; + + if (!key) continue; + + pKey = PyString_FromString(key); + if (!pKey) continue; + + if (PyDict_Contains(dict, pKey)) { + WARN("rlm_python: Ignoring duplicate config section '%s'", key); + continue; + } + + if (!(sub_dict = PyDict_New())) { + WARN("rlm_python: Unable to create subdict for config section '%s'", key); + } + + (void)PyDict_SetItem(dict, pKey, sub_dict); + + python_parse_config(sub_cs, lvl + 1, sub_dict); + } else if (cf_item_is_pair(ci)) { + CONF_PAIR *cp = cf_item_to_pair(ci); + char const *key = cf_pair_attr(cp); /* dict key */ + char const *value = cf_pair_value(cp); /* dict value */ + PyObject *pKey, *pValue; + + if (!key || !value) continue; + + pKey = PyString_FromString(key); + pValue = PyString_FromString(value); + if (!pKey || !pValue) continue; + + /* + * This is an item. + * Store item attr / value in current dict. + */ + if (PyDict_Contains(dict, pKey)) { + WARN("rlm_python: Ignoring duplicate config item '%s'", key); + continue; + } + + (void)PyDict_SetItem(dict, pKey, pValue); + + DEBUG("%*s%s = %s", indent_item, " ", key, value); + } + } + + DEBUG("%*s}", indent_section, " "); +} + +#ifdef HAVE_DL_ITERATE_PHDR +static int dlopen_libpython_cb(struct dl_phdr_info *info, + UNUSED size_t size, void *data) +{ + const char *pattern = "/" LIBPYTHON_LINKER_NAME; + char **ppath = (char **)data; + + if (strstr(info->dlpi_name, pattern) != NULL) { + if (*ppath != NULL) { + talloc_free(*ppath); + *ppath = NULL; + return EEXIST; + } else { + *ppath = talloc_strdup(NULL, info->dlpi_name); + if (*ppath == NULL) { + return errno; + } + } + } + return 0; +} + +/* Dlopen the already linked libpython */ +static void *dlopen_libpython(int flags) +{ + char *path = NULL; + int rc; + void *handle; + + /* Find the linked libpython path */ + rc = dl_iterate_phdr(dlopen_libpython_cb, &path); + if (rc != 0) { + WARN("Failed searching for libpython " + "among linked libraries: %s", strerror(rc)); + return NULL; + } else if (path == NULL) { + WARN("Libpython is not found among linked libraries"); + return NULL; + } + + /* Dlopen the found library */ + handle = dlopen(path, flags); + if (handle == NULL) { + WARN("Failed loading %s: %s", path, dlerror()); + } + talloc_free(path); + return handle; +} +#else /* ! HAVE_DL_ITERATE_PHDR */ +/* Dlopen libpython by its linker name (bare soname) */ +static void *dlopen_libpython(int flags) +{ + const char *name = LIBPYTHON_LINKER_NAME; + void *handle; + handle = dlopen(name, flags); + if (handle == NULL) { + WARN("Failed loading %s: %s", name, dlerror()); + } + return handle; +} +#endif /* ! HAVE_DL_ITERATE_PHDR */ + +/** Initialises a separate python interpreter for this module instance + * + */ +static int python_interpreter_init(rlm_python_t *inst, CONF_SECTION *conf) +{ + int i; + + /* + * Explicitly load libpython, so symbols will be available to lib-dynload modules + */ + if (python_instances == 0) { + INFO("Python version: %s", Py_GetVersion()); + + python_dlhandle = dlopen_libpython(RTLD_NOW | RTLD_GLOBAL); + if (!python_dlhandle) WARN("Failed loading libpython symbols into global symbol table"); + +#if PY_VERSION_HEX > 0x03050000 + { + inst->wide_name = Py_DecodeLocale(main_config.name, strlen(main_config.name)); + Py_SetProgramName(inst->wide_name); /* The value of argv[0] as a wide char string */ + } +#else + { + char *name; + + memcpy(&name, &main_config.name, sizeof(name)); + Py_SetProgramName(name); /* The value of argv[0] as a wide char string */ + } +#endif + + Py_InitializeEx(0); /* Don't override signal handlers - noop on subs calls */ + PyEval_InitThreads(); /* This also grabs a lock (which we then need to release) */ + main_interpreter = PyThreadState_Get(); /* Store reference to the main interpreter */ + } + rad_assert(PyEval_ThreadsInitialized()); + + /* + * Increment the reference counter + */ + python_instances++; + + /* + * This sets up a separate environment for each python module instance + * These will be destroyed on Py_Finalize(). + */ + if (!inst->cext_compat) { + inst->sub_interpreter = Py_NewInterpreter(); + } else { + inst->sub_interpreter = main_interpreter; + } + + PyThreadState_Swap(inst->sub_interpreter); + + /* + * Due to limitations in Python, sub-interpreters don't work well + * with Python C extensions if they use GIL lock functions. + */ + if (!inst->cext_compat || !main_module) { + CONF_SECTION *cs; + + /* + * Set the python search path + * + * The path buffer does not appear to be dup'd + * so its lifetime should really be bound to + * the lifetime of the module. + */ + if (inst->python_path) { + char *p, *path; + PyObject *sys = PyImport_ImportModule("sys"); + PyObject *sys_path = PyObject_GetAttrString(sys, "path"); + + memcpy(&p, &inst->python_path, sizeof(path)); + + for (path = strtok(p, ":"); path != NULL; path = strtok(NULL, ":")) { + PyList_Append(sys_path, PyString_FromString(path)); + } + + PyObject_SetAttrString(sys, "path", sys_path); + Py_DecRef(sys); + Py_DecRef(sys_path); + } + + /* + * Initialise a new module, with our default methods + */ + inst->module = Py_InitModule3("radiusd", module_methods, "FreeRADIUS python module"); + if (!inst->module) { + error: + python_error_log(); + PyEval_SaveThread(); + return -1; + } + + /* + * Py_InitModule3 returns a borrowed ref, the actual + * module is owned by sys.modules, so we also need + * to own the module to prevent it being freed early. + */ + Py_IncRef(inst->module); + + if (inst->cext_compat) main_module = inst->module; + + for (i = 0; radiusd_constants[i].name; i++) { + if ((PyModule_AddIntConstant(inst->module, radiusd_constants[i].name, + radiusd_constants[i].value)) < 0) + goto error; + } + + /* + * Convert a FreeRADIUS config structure into a python + * dictionary. + */ + inst->pythonconf_dict = PyDict_New(); + if (!inst->pythonconf_dict) { + ERROR("Unable to create python dict for config"); + python_error_log(); + return -1; + } + + /* + * Add module configuration as a dict + */ + if (PyModule_AddObject(inst->module, "config", inst->pythonconf_dict) < 0) goto error; + + cs = cf_section_sub_find(conf, "config"); + if (cs) python_parse_config(cs, 0, inst->pythonconf_dict); + } else { + inst->module = main_module; + Py_IncRef(inst->module); + inst->pythonconf_dict = PyObject_GetAttrString(inst->module, "config"); + Py_IncRef(inst->pythonconf_dict); + } + + PyEval_SaveThread(); + + return 0; +} + +/* + * Do any per-module initialization that is separate to each + * configured instance of the module. e.g. set up connections + * to external databases, read configuration files, set up + * dictionary entries, etc. + * + * If configuration information is given in the config section + * that must be referenced in later calls, store a handle to it + * in *instance otherwise put a null pointer there. + * + */ +static int mod_instantiate(CONF_SECTION *conf, void *instance) +{ + rlm_python_t *inst = instance; + int code = 0; + + inst->name = cf_section_name2(conf); + if (!inst->name) inst->name = cf_section_name1(conf); + + /* + * Load the python code required for this module instance + */ + if (python_interpreter_init(inst, conf) < 0) return -1; + + /* + * Switch to our module specific main thread + */ + PyEval_RestoreThread(inst->sub_interpreter); + + /* + * Process the various sections + */ +#define PYTHON_FUNC_LOAD(_x) if (python_function_load(#_x, &inst->_x) < 0) goto error + PYTHON_FUNC_LOAD(instantiate); + PYTHON_FUNC_LOAD(authenticate); + PYTHON_FUNC_LOAD(authorize); + PYTHON_FUNC_LOAD(preacct); + PYTHON_FUNC_LOAD(accounting); + PYTHON_FUNC_LOAD(checksimul); + PYTHON_FUNC_LOAD(pre_proxy); + PYTHON_FUNC_LOAD(post_proxy); + PYTHON_FUNC_LOAD(post_auth); +#ifdef WITH_COA + PYTHON_FUNC_LOAD(recv_coa); + PYTHON_FUNC_LOAD(send_coa); +#endif + PYTHON_FUNC_LOAD(detach); + + /* + * Call the instantiate function only if the function and module is set. + */ + if (inst->instantiate.module_name && inst->instantiate.function_name) { + code = do_python_single(NULL, inst->instantiate.function, "instantiate", inst->pass_all_vps, inst->pass_all_vps_dict); + if (code == RLM_MODULE_FAIL) { + error: + python_error_log(); /* Needs valid thread with GIL */ + PyEval_SaveThread(); + return -1; + } + } + PyEval_SaveThread(); + + return 0; +} + +static int mod_detach(void *instance) +{ + rlm_python_t *inst = instance; + int ret; + + /* + * Call module destructor + */ + PyEval_RestoreThread(inst->sub_interpreter); + + ret = do_python_single(NULL, inst->detach.function, "detach", inst->pass_all_vps, inst->pass_all_vps_dict); + if (ret == RLM_MODULE_FAIL) python_error_log(); + +#define PYTHON_FUNC_DESTROY(_x) python_function_destroy(&inst->_x) + PYTHON_FUNC_DESTROY(instantiate); + PYTHON_FUNC_DESTROY(authorize); + PYTHON_FUNC_DESTROY(authenticate); + PYTHON_FUNC_DESTROY(preacct); + PYTHON_FUNC_DESTROY(accounting); + PYTHON_FUNC_DESTROY(checksimul); + PYTHON_FUNC_DESTROY(detach); + + Py_DecRef(inst->pythonconf_dict); + Py_DecRef(inst->module); + + PyEval_SaveThread(); + + /* + * Force cleaning up of threads if this is *NOT* a worker + * thread, which happens if this is being called from + * unittest framework, and probably with the server running + * in debug mode. + */ + rbtree_free(local_thread_state); + local_thread_state = NULL; + + /* + * Only destroy if it's a subinterpreter + */ + if (!inst->cext_compat) python_interpreter_free(inst->sub_interpreter); + + if ((--python_instances) == 0) { + PyThreadState_Swap(main_interpreter); /* Swap to the main thread */ + Py_Finalize(); + dlclose(python_dlhandle); + +#if PY_VERSION_HEX > 0x03050000 + if (inst->wide_name) PyMem_RawFree(inst->wide_name); +#endif + } + + + return ret; +} + +/* + * The module name should be the only globally exported symbol. + * That is, everything else should be 'static'. + * + * If the module needs to temporarily modify it's instantiation + * data, the type should be changed to RLM_TYPE_THREAD_UNSAFE. + * The server will then take care of ensuring that the module + * is single-threaded. + */ +extern module_t rlm_python; +module_t rlm_python = { + .magic = RLM_MODULE_INIT, + .name = "python", + .type = RLM_TYPE_THREAD_UNSAFE, + .inst_size = sizeof(rlm_python_t), + .config = module_config, + .instantiate = mod_instantiate, + .detach = mod_detach, + .methods = { + [MOD_AUTHENTICATE] = mod_authenticate, + [MOD_AUTHORIZE] = mod_authorize, + [MOD_PREACCT] = mod_preacct, + [MOD_ACCOUNTING] = mod_accounting, + [MOD_SESSION] = mod_checksimul, + [MOD_PRE_PROXY] = mod_pre_proxy, + [MOD_POST_PROXY] = mod_post_proxy, + [MOD_POST_AUTH] = mod_post_auth, +#ifdef WITH_COA + [MOD_RECV_COA] = mod_recv_coa, + [MOD_SEND_COA] = mod_send_coa +#endif + } +}; |