/*
 * rlm_python.c
 *
 *
 *   This program 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 * Copyright 2000  The FreeRADIUS server project
 * Copyright 2002  Miguel A.L. Paraz <mparaz@mparaz.com>
 * Copyright 2002  Imperium Technology, Inc.
 */

#include <Python.h>

#include "autoconf.h"
#include "libradius.h"

#include <stdio.h>
#include <stdlib.h>

#include "radiusd.h"
#include "modules.h"
#include "conffile.h"

#define Pyx_BLOCK_THREADS       {PyGILState_STATE __gstate = PyGILState_Ensure();
#define Pyx_UNBLOCK_THREADS     PyGILState_Release(__gstate);}



static const char rcsid[] = "$Id: rlm_python.c,v 1.6.4.1 2007/03/05 14:15:28 pnixon Exp $";

/*
 *	Define a structure for our module configuration.
 *
 *	These variables do not need to be in a structure, but it's
 *	a lot cleaner to do so, and a pointer to the structure can
 *	be used as the instance handle.
 */

struct rlm_python_t {
        char    *mod_instantiate;
        char    *mod_authorize;
        char    *mod_authenticate;
        char    *mod_preacct;
        char    *mod_accounting;
        char    *mod_checksimul;
        char    *mod_detach;

        /* Names of functions */
        char    *func_instantiate;
        char    *func_authorize;
        char    *func_authenticate;
        char    *func_preacct;
        char    *func_accounting;
        char    *func_checksimul;
        char    *func_detach;

        PyObject *pModule_instantiate;
        PyObject *pModule_authorize;
        PyObject *pModule_authenticate;
        PyObject *pModule_preacct;
        PyObject *pModule_accounting;
        PyObject *pModule_checksimul;
        PyObject *pModule_detach;

        /* Functions */
        PyObject *pFunc_instantiate;
        PyObject *pFunc_authorize;
        PyObject *pFunc_authenticate;
        PyObject *pFunc_preacct;
        PyObject *pFunc_accounting;
        PyObject *pFunc_checksimul;
        PyObject *pFunc_detach;
};


/*
 *	A mapping of configuration file names to internal variables.
 *
 *	Note that the string is dynamically allocated, so it MUST
 *	be freed.  When the configuration file parse re-reads the string,
 *	it free's the old one, and strdup's the new one, placing the pointer
 *	to the strdup'd string into 'config.string'.  This gets around
 *	buffer over-flows.
 */
static CONF_PARSER module_config[] = {
  { "mod_instantiate",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_instantiate), NULL,  NULL},
  { "func_instantiate",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_instantiate), NULL,  NULL},

  { "mod_authorize",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_authorize), NULL,  NULL},
  { "func_authorize",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_authorize), NULL,  NULL},

  { "mod_authenticate",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_authenticate), NULL,  NULL},
  { "func_authenticate",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_authenticate), NULL,  NULL},

  { "mod_preacct",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_preacct), NULL,  NULL},
  { "func_preacct",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_preacct), NULL,  NULL},

  { "mod_accounting",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_accounting), NULL,  NULL},
  { "func_accounting",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_accounting), NULL,  NULL},

  { "mod_checksimul",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_checksimul), NULL,  NULL},
  { "func_checksimul",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_checksimul), NULL,  NULL},

  { "mod_detach",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, mod_detach), NULL,  NULL},
  { "func_detach",  PW_TYPE_STRING_PTR,
    offsetof(struct rlm_python_t, func_detach), NULL,  NULL},


  { NULL, -1, 0, NULL, NULL }		/* end the list */
};

static struct {
        const char*     name;
        int             value;
} radiusd_constants[] = {
        { "L_DBG",              L_DBG                   },
        { "L_AUTH",             L_AUTH                  },
        { "L_INFO",             L_INFO                  },
        { "L_ERR",              L_ERR                   },
        { "L_PROXY",            L_PROXY                 },
        { "L_CONS",             L_CONS                  },
        { "RLM_MODULE_REJECT",  RLM_MODULE_REJECT       },
        { "RLM_MODULE_FAIL",    RLM_MODULE_FAIL         },
        { "RLM_MODULE_OK",      RLM_MODULE_OK           },
        { "RLM_MODULE_HANDLED", RLM_MODULE_HANDLED      },
        { "RLM_MODULE_INVALID", RLM_MODULE_INVALID      },
        { "RLM_MODULE_USERLOCK",RLM_MODULE_USERLOCK     },
        { "RLM_MODULE_NOTFOUND",RLM_MODULE_NOTFOUND     },
        { "RLM_MODULE_NOOP",    RLM_MODULE_NOOP         },
        { "RLM_MODULE_UPDATED", RLM_MODULE_UPDATED      },
        { "RLM_MODULE_NUMCODES",RLM_MODULE_NUMCODES     },
        { NULL, 0 },
};


/* Let assume that radiusd module is only one since we have only one intepreter */

static PyObject *radiusd_module = NULL;

/*
 * radiusd Python functions
 */

/* radlog wrapper */
static PyObject *python_radlog(const 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 radiusd_methods[] = {
    {"radlog", (PyCFunction) &python_radlog, METH_VARARGS, "freeradius radlog()."},
    {NULL, NULL, 0, NULL}
};

static void python_error() {
    PyObject        *pType = NULL;
    PyObject        *pValue = NULL;
    PyObject        *pTraceback = NULL;
    PyObject        *pStr1 = NULL;
    PyObject        *pStr2 = NULL;

    Pyx_BLOCK_THREADS

    PyErr_Fetch(&pType, &pValue, &pTraceback);
    if (pType == NULL || pValue == NULL)
        goto failed;
    if ((pStr1 = PyObject_Str(pType)) == NULL || (pStr2 = PyObject_Str(pValue)) == NULL)
        goto failed;
    radlog(L_ERR, "rlm_python:EXCEPT:%s: %s", PyString_AsString(pStr1), PyString_AsString(pStr2));

failed:
    Py_XDECREF(pStr1);
    Py_XDECREF(pStr2);
    Py_XDECREF(pType);
    Py_XDECREF(pValue);
    Py_XDECREF(pTraceback);

    Pyx_UNBLOCK_THREADS
}

static int python_init()
{
    int i;

    Py_SetProgramName("radiusd");

    Py_Initialize();

    PyEval_InitThreads(); // This also grabs a lock

    if ((radiusd_module = Py_InitModule3("radiusd", radiusd_methods, "FreeRADIUS Module.")) == NULL)
        goto failed;

    for (i = 0; radiusd_constants[i].name; i++)
        if ((PyModule_AddIntConstant(radiusd_module, radiusd_constants[i].name, radiusd_constants[i].value)) < 0)
            goto failed;

    PyEval_ReleaseLock(); // Drop lock grabbed by InitThreads

    radlog(L_DBG, "python_init done");

    return 0;

failed:
    python_error();
    Py_Finalize();
    return -1;
}

static int python_destroy() {
    Pyx_BLOCK_THREADS
    Py_XDECREF(radiusd_module);
    Py_Finalize();
    Pyx_UNBLOCK_THREADS

    return 0;
}

static void python_vptuple(VALUE_PAIR **vpp, PyObject *pValue, const char *funcname) {
        int             i;
        int             tuplesize;
        VALUE_PAIR      *vp;

        /* If the Python function gave us None for the tuple, then just return. */
        if (pValue == Py_None)
                return;

        if (!PyTuple_CheckExact(pValue)) {
                radlog(L_ERR, "rlm_python:%s: non-tuple passed", funcname);
                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;
                int pairsize;
                const char *s1;
                const char *s2;

                if (!PyTuple_CheckExact(pTupleElement)) {
                        radlog(L_ERR, "rlm_python:%s: tuple element %d is not a tuple", funcname, i);
                        continue;
                }
                /* Check if it's a pair */
                if ((pairsize = PyTuple_GET_SIZE(pTupleElement)) != 2) {
                        radlog(L_ERR, "rlm_python:%s: tuple element %d is a tuple of size %d. Must be 2", funcname, i, pairsize);
                        continue;
                }
                pStr1 = PyTuple_GET_ITEM(pTupleElement, 0);
                pStr2 = PyTuple_GET_ITEM(pTupleElement, 1);
                if ((!PyString_CheckExact(pStr1)) || (!PyString_CheckExact(pStr2))) {
                        radlog(L_ERR, "rlm_python:%s: tuple element %d must be as (str, str)", funcname, i);
                        continue;
                }
                s1 = PyString_AsString(pStr1);
                s2 = PyString_AsString(pStr2);
                /* xxx Might need to support other T_OP */
                vp = pairmake(s1, s2, T_OP_EQ);
                if (vp != NULL) {
                        pairadd(vpp, vp);
                        radlog(L_DBG, "rlm_python:%s: '%s' = '%s'", funcname, s1, s2);
                } else {
                        radlog(L_DBG, "rlm_python:%s: Failed: '%s' = '%s'", funcname, s1, s2);
                }
        }
}


/* This is the core Python function that the others wrap around.
 * Pass the value-pair print strings in a tuple.
 * xxx We're not checking the errors. If we have errors, what do we do?
 */

static int python_function(REQUEST *request, PyObject *pFunc, const char *funcname) {
        char            buf[1024];
        VALUE_PAIR      *vp;
        PyObject        *pRet = NULL;
        PyObject        *pArgs = NULL;
        int             tuplelen;
        int             ret;

	PyGILState_STATE gstate;

        /* Return with "OK, continue" if the function is not defined. */
        if (pFunc == NULL)
                return RLM_MODULE_OK;

        /* Default return value is "OK, continue" */
        ret = RLM_MODULE_OK;

        /* 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 packet.
         * If request is NULL, pass None.
         */
        tuplelen = 0;
        if (request != NULL) {
                for (vp = request->packet->vps; vp; vp = vp->next)
                        tuplelen++;
        }

        gstate = PyGILState_Ensure();

        if (tuplelen == 0) {
                Py_INCREF(Py_None);
                pArgs = Py_None;
        } else {
                int     i = 0;
                if ((pArgs = PyTuple_New(tuplelen)) == NULL)
                        goto failed;
                for (vp = request->packet->vps; vp != NULL; vp = vp->next, i++) {
                        PyObject *pPair;
                        PyObject *pStr;
                        /* The inside tuple has two only: */
                        if ((pPair = PyTuple_New(2)) == NULL)
                                goto failed;
                        /* Put the tuple inside the container */
                        PyTuple_SET_ITEM(pArgs, i, pPair);
                        /* The name. logic from vp_prints, lib/print.c */
                        if (vp->flags.has_tag)
                                snprintf(buf, sizeof(buf), "%s:%d", vp->name, vp->flags.tag);
                        else
                                strcpy(buf, vp->name);
                        if ((pStr = PyString_FromString(buf)) == NULL)
                                goto failed;
                        PyTuple_SET_ITEM(pPair, 0, pStr);
                        vp_prints_value(buf, sizeof(buf), vp, 1);
                        if ((pStr = PyString_FromString(buf)) == NULL)
                                goto failed;
                        PyTuple_SET_ITEM(pPair, 1, pStr);
                }
        }

        /* Call Python function. */
        pRet = PyObject_CallFunctionObjArgs(pFunc, pArgs, NULL);

	if (pRet == NULL)
                goto failed;

        if (request == NULL)
                goto okay;
        /* The function returns either:
         *  1. tuple containing the integer return value,
         *  then the integer reply code (or None to not set),
         *  then the string tuples to build the reply with.
         *  (returnvalue, (p1, s1), (p2, s2))
         *
         *  2. the function return value alone
         *
         *  3. None - default return value is set
         *
         * xxx This code is messy!
         */
        if (PyTuple_CheckExact(pRet)) {
                PyObject *pTupleInt;

                if (PyTuple_GET_SIZE(pRet) != 3) {
                        radlog(L_ERR, "rlm_python:%s: tuple must be (return, replyTuple, configTuple)", funcname);
                        goto failed;
                }
                pTupleInt = PyTuple_GET_ITEM(pRet, 0);
                if (!PyInt_CheckExact(pTupleInt)) {
                        radlog(L_ERR, "rlm_python:%s: first tuple element not an integer", funcname);
                        goto failed;
                }
                /* Now have the return value */
                ret = PyInt_AsLong(pTupleInt);
                /* Reply item tuple */
                python_vptuple(&request->reply->vps, PyTuple_GET_ITEM(pRet, 1), funcname);
                /* Config item tuple */
                python_vptuple(&request->config_items, PyTuple_GET_ITEM(pRet, 2), funcname);
        } 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 */
                radlog(L_ERR, "rlm_python:%s: function did not return a tuple or None", funcname);
                goto failed;
        }
        if (ret == RLM_MODULE_REJECT && request != NULL)
                pairfree(&request->reply->vps);
okay:
        Py_DECREF(pArgs);
        Py_DECREF(pRet);
	PyGILState_Release(gstate);
        return ret;
failed:
        python_error();
        Py_XDECREF(pArgs);
        Py_XDECREF(pRet);
        PyGILState_Release(gstate);

        return -1;
}

/*
 * Import a user module and load a function from it
 */

static int python_load_function(char *module, const char *func, PyObject **pModule, PyObject **pFunc) {
        const char      funcname[] = "python_load_function";
        PyGILState_STATE gstate;

        *pFunc = NULL;
        *pModule = NULL;
        gstate = PyGILState_Ensure();

        if (module != NULL && func != NULL) {
                if ((*pModule = PyImport_ImportModule(module)) == NULL) {
                        radlog(L_ERR, "rlm_python:%s: module '%s' is not found", funcname, module);
                        goto failed;
                }
                if ((*pFunc = PyObject_GetAttrString(*pModule, func)) == NULL) {
                        radlog(L_ERR, "rlm_python:%s: function '%s.%s' is not found", funcname, module, func);
                        goto failed;
                }
                if (!PyCallable_Check(*pFunc)) {
                        radlog(L_ERR, "rlm_python:%s: function '%s.%s' is not callable", funcname, module, func);
                        goto failed;
                }
        }
	PyGILState_Release(gstate);
        return 0;
failed:
        python_error();
        radlog(L_ERR, "rlm_python:%s: failed to import python function '%s.%s'", funcname, module, func);
        Py_XDECREF(*pFunc);
        *pFunc = NULL;
        Py_XDECREF(*pModule);
        PyGILState_Release(gstate);
        *pModule = NULL;
        return -1;
}

static void python_objclear(PyObject **ob) {
        if (*ob != NULL) {
		Pyx_BLOCK_THREADS
                Py_DECREF(*ob);
                Pyx_UNBLOCK_THREADS
                *ob = NULL;
        }
}

static void python_instance_clear(struct rlm_python_t *data) {
        python_objclear(&data->pFunc_instantiate);
        python_objclear(&data->pFunc_authorize);
        python_objclear(&data->pFunc_authenticate);
        python_objclear(&data->pFunc_preacct);
        python_objclear(&data->pFunc_accounting);
        python_objclear(&data->pFunc_checksimul);
        python_objclear(&data->pFunc_detach);

        python_objclear(&data->pModule_instantiate);
        python_objclear(&data->pModule_authorize);
        python_objclear(&data->pModule_authenticate);
        python_objclear(&data->pModule_preacct);
        python_objclear(&data->pModule_accounting);
        python_objclear(&data->pModule_checksimul);
        python_objclear(&data->pModule_detach);
}

/*
 *	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 python_instantiate(CONF_SECTION *conf, void **instance) {
        struct rlm_python_t    *data = NULL;

        /*
         *      Set up a storage area for instance data
         */
        if ((data = malloc(sizeof(*data))) == NULL)
                return -1;
        bzero(data, sizeof(*data));

        /*
         *      If the configuration parameters can't be parsed, then
         *      fail.
         */
        if (cf_section_parse(conf, data, module_config) < 0) {
                free(data);
                return -1;
        }

        /*
         * Import user modules.
         */
        if (python_load_function(data->mod_instantiate,
                                data->func_instantiate,
                                &data->pModule_instantiate,
                                &data->pFunc_instantiate) < 0)
                goto failed;

        if (python_load_function(data->mod_authenticate,
                                data->func_authenticate,
                                &data->pModule_authenticate,
                                &data->pFunc_authenticate) < 0)
                goto failed;

        if (python_load_function(data->mod_authorize,
                                data->func_authorize,
                                &data->pModule_authorize,
                                &data->pFunc_authorize) < 0)
                goto failed;

        if (python_load_function(data->mod_preacct,
                                data->func_preacct,
                                &data->pModule_preacct,
                                &data->pFunc_preacct) < 0)
                goto failed;

        if (python_load_function(data->mod_accounting,
                                data->func_accounting,
                                &data->pModule_accounting,
                                &data->pFunc_accounting) < 0)
                goto failed;

        if (python_load_function(data->mod_checksimul,
                                data->func_checksimul,
                                &data->pModule_checksimul,
                                &data->pFunc_checksimul) < 0)
                goto failed;

        if (python_load_function(data->mod_detach,
                                data->func_detach,
                                &data->pModule_detach,
                                &data->pFunc_detach) < 0)
                goto failed;

        *instance = data;
        /* Call the instantiate function.  No request.  Use the return value. */

        return python_function(NULL, data->pFunc_instantiate, "instantiate");
failed:
        python_error();
        python_instance_clear(data);
        return -1;
}

static int python_detach(void *instance) {
        struct rlm_python_t    *data = (struct rlm_python_t *) instance;
        int             ret;

        ret = python_function(NULL, data->pFunc_detach, "detach");

        python_instance_clear(data);

        free(data);
        return ret;
}


/* Wrapper functions */
static int python_authorize(void *instance, REQUEST *request)
{
    return python_function(request,
			   ((struct rlm_python_t *)instance)->pFunc_authorize,
			   "authorize");
}

static int python_authenticate(void *instance, REQUEST *request)
{
    return python_function(
	request,
	((struct rlm_python_t *)instance)->pFunc_authenticate,
	"authenticate");
}

static int python_preacct(void *instance, REQUEST *request)
{
    return python_function(
	request,
	((struct rlm_python_t *)instance)->pFunc_preacct,
	"preacct");
}

static int python_accounting(void *instance, REQUEST *request)
{
    return python_function(
	request,
	((struct rlm_python_t *)instance)->pFunc_accounting,
	"accounting");
}

static int python_checksimul(void *instance, REQUEST *request)
{
    return python_function(
	request,
	((struct rlm_python_t *)instance)->pFunc_checksimul,
	"checksimul");
}


/*
 *	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.
 */
module_t rlm_python = {
	"python",
	RLM_TYPE_THREAD_SAFE,		/* type */
	python_init,			/* initialization */
	python_instantiate,		/* instantiation */
	{
		python_authenticate,	/* authentication */
		python_authorize,	/* authorization */
		python_preacct,		/* preaccounting */
		python_accounting,	/* accounting */
		python_checksimul,	/* checksimul */
		NULL,			/* pre-proxy */
		NULL,			/* post-proxy */
		NULL			/* post-auth */
	},
	python_detach,			/* detach */
	python_destroy,				/* destroy */
};
