/*
 *  Dynamic Loadable Agent Modules MIB (UCD-DLMOD-MIB) - dlmod.c
 *
 */
#include <net-snmp/net-snmp-config.h>
#include <net-snmp/net-snmp-features.h>

#include <ctype.h>
#if HAVE_STDLIB_H
#include <stdlib.h>
#endif
#include <stdio.h>
#if HAVE_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif

#include <net-snmp/net-snmp-includes.h>
#include <net-snmp/agent/net-snmp-agent-includes.h>

#if defined(WIN32)
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include "dlmod.h"

#ifndef SNMPDLMODPATH
#define SNMPDLMODPATH "/usr/local/lib/snmp/dlmod"
#endif

struct dlmod {
  struct dlmod   *next;
  int             index;
  char            name[64 + 1];
  char            path[255 + 1];
  char           *error;
  void           *handle;
  int             status;
};

#define DLMOD_LOADED		1
#define DLMOD_UNLOADED		2
#define DLMOD_ERROR		3
#define DLMOD_LOAD		4
#define DLMOD_UNLOAD		5
#define DLMOD_CREATE		6
#define DLMOD_DELETE		7

static struct dlmod *dlmods;
static unsigned int dlmod_next_index = 1;
static char     dlmod_path[1024];

static struct dlmod   *
dlmod_create_module(void)
{
    struct dlmod  **pdlmod, *dlm;

    DEBUGMSGTL(("dlmod", "dlmod_create_module\n"));
    dlm = calloc(1, sizeof(struct dlmod));
    if (dlm == NULL)
        return NULL;

    dlm->index = dlmod_next_index++;
    dlm->status = DLMOD_UNLOADED;

    for (pdlmod = &dlmods; *pdlmod != NULL; pdlmod = &((*pdlmod)->next))
        ;
    *pdlmod = dlm;

    return dlm;
}

static void
dlmod_delete_module(struct dlmod *dlm)
{
    struct dlmod  **pdlmod;

    DEBUGMSGTL(("dlmod", "dlmod_delete_module\n"));
    if (!dlm || dlm->status != DLMOD_UNLOADED)
        return;

    for (pdlmod = &dlmods; *pdlmod; pdlmod = &((*pdlmod)->next))
        if (*pdlmod == dlm) {
            *pdlmod = dlm->next;
            free(dlm->error);
            free(dlm);
            return;
        }
}

#if defined(WIN32)
/*
 * See also Microsoft, "Overview of x64 Calling Conventions", MSDN
 * (http://msdn.microsoft.com/en-us/library/ms235286.aspx).
 */
#ifdef _M_X64
typedef int (*dl_function_ptr)(void);
#else
typedef int (__stdcall *dl_function_ptr)(void);
#endif
#else
typedef int (*dl_function_ptr)(void);
#endif

#if defined(WIN32)
static const char dlmod_dl_suffix[] = "dll";
#else
static const char dlmod_dl_suffix[] = "so";
#endif

static void* dlmod_dlopen(const char *path)
{
#if defined(WIN32)
    return LoadLibrary(path);
#elif defined(RTLD_NOW)
    return dlopen(path, RTLD_NOW);
#else
    return dlopen(path, RTLD_LAZY);
#endif
}

static void dlmod_dlclose(void *handle)
{
#if defined(WIN32)
    FreeLibrary(handle);
#else
    dlclose(handle);
#endif
}

static void *dlmod_dlsym(void *handle, const char *symbol)
{
#if defined(WIN32)
    return GetProcAddress(handle, symbol);
#else
    return dlsym(handle, symbol);
#endif
}

static const char *dlmod_dlerror(void)
{
#if defined(WIN32)
    static char errstr[256];
    const DWORD dwErrorcode = GetLastError();
    LPTSTR      lpMsgBuf;

    FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                  NULL, dwErrorcode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
                  (LPTSTR) &lpMsgBuf, 0, NULL);
    if (lpMsgBuf) {
        LPTSTR          p;

        /*
         * Remove trailing "\r\n".
         */
        p = strchr(lpMsgBuf, '\r');
        if (p)
            *p = '\0';
        snprintf(errstr, sizeof(errstr), "%s", lpMsgBuf);
        LocalFree(lpMsgBuf);
    } else {
        snprintf(errstr, sizeof(errstr), "error code %ld", dwErrorcode);
    }
    return errstr;
#else
    return dlerror();
#endif
}

static int dlmod_is_abs_path(const char *path)
{
#if defined(WIN32)
    return (strncmp(path, "//", 2) == 0 || strncmp(path, "\\\\", 2) == 0) ||
        (isalpha((u_char)path[0]) && path[1] == ':' &&
         (path[2] == '/' || path[2] == '\\'));
#else
    return path[0] == '/';
#endif
}

static void
dlmod_load_module(struct dlmod *dlm)
{
    DEBUGMSGTL(("dlmod", "dlmod_load_module %s: %s\n", dlm->name,
                dlm->path));

    if (!dlm || (dlm->status != DLMOD_UNLOADED && dlm->status != DLMOD_ERROR))
        return;

    free(dlm->error);
    dlm->error = NULL;

    if (dlmod_is_abs_path(dlm->path)) {
        dlm->handle = dlmod_dlopen(dlm->path);
        if (dlm->handle == NULL) {
            if (asprintf(&dlm->error, "dlopen(%s) failed: %s", dlm->path,
                         dlmod_dlerror()) < 0)
                dlm->error = NULL;
            dlm->status = DLMOD_ERROR;
            return;
        }
    } else {
        char *st, *p, *tmp_path = NULL;

        for (p = strtok_r(dlmod_path, ENV_SEPARATOR, &st); p;
             p = strtok_r(NULL, ENV_SEPARATOR, &st)) {
            free(tmp_path);
            if (asprintf(&tmp_path, "%s/%s.%s", p, dlm->path, dlmod_dl_suffix)
                < 0) {
                dlm->status = DLMOD_ERROR;
                return;
            }
            DEBUGMSGTL(("dlmod", "p: %s tmp_path: %s\n", p, tmp_path));
            dlm->handle = tmp_path ? dlmod_dlopen(tmp_path) : NULL;
            if (dlm->handle == NULL) {
                free(dlm->error);
                if (asprintf(&dlm->error, "dlopen(%s) failed: %s", tmp_path,
                             dlmod_dlerror()) < 0)
                    dlm->error = NULL;
                dlm->status = DLMOD_ERROR;
            }
        }
        strlcpy(dlm->path, tmp_path, sizeof(dlm->path));
        free(tmp_path);
        if (dlm->status == DLMOD_ERROR)
            return;
    }
    {
        char sym_init[64 + sizeof("init_")];
        dl_function_ptr dl_init;

        snprintf(sym_init, sizeof(sym_init), "init_%s", dlm->name);
        dl_init = dlmod_dlsym(dlm->handle, sym_init);
        if (dl_init == NULL) {
            dlmod_dlclose(dlm->handle);
            free(dlm->error);
            if (asprintf(&dlm->error, "dlsym failed: can't find \'%s\'",
                         sym_init) < 0)
                dlm->error = NULL;
            dlm->status = DLMOD_ERROR;
            return;
        }
        dl_init();
    }

    dlm->error = NULL;
    dlm->status = DLMOD_LOADED;
}

static void
dlmod_unload_module(struct dlmod *dlm)
{
    char            sym_deinit[64 + sizeof("shutdown_")];
    dl_function_ptr dl_deinit;

    if (!dlm || dlm->status != DLMOD_LOADED)
        return;

    snprintf(sym_deinit, sizeof(sym_deinit), "deinit_%s", dlm->name);
    dl_deinit = dlmod_dlsym(dlm->handle, sym_deinit);
    if (!dl_deinit) {
        snprintf(sym_deinit, sizeof(sym_deinit), "shutdown_%s", dlm->name);
        dl_deinit = dlmod_dlsym(dlm->handle, sym_deinit);
    }
    if (dl_deinit) {
        DEBUGMSGTL(("dlmod", "Calling %s()\n", sym_deinit));
        dl_deinit();
    } else {
        DEBUGMSGTL(("dlmod", "No destructor for %s\n", dlm->name));
    }
    dlmod_dlclose(dlm->handle);
    dlm->status = DLMOD_UNLOADED;
    DEBUGMSGTL(("dlmod", "Module %s unloaded\n", dlm->name));
}

static struct dlmod   *
dlmod_get_by_index(int iindex)
{
    struct dlmod   *dlmod;

    for (dlmod = dlmods; dlmod; dlmod = dlmod->next)
        if (dlmod->index == iindex)
            return dlmod;

    return NULL;
}

/*
 * Functions to parse config lines
 */

static void
dlmod_parse_config(const char *token, char *cptr)
{
    char           *dlm_name, *dlm_path;
    struct dlmod   *dlm;
    char           *st;

    if (cptr == NULL) {
        config_perror("Bad dlmod line");
        return;
    }
    /*
     * remove comments
     */
    *(cptr + strcspn(cptr, "#;\r\n")) = '\0';

    dlm = dlmod_create_module();
    if (!dlm)
        return;

    /*
     * dynamic module name
     */
    dlm_name = strtok_r(cptr, "\t ", &st);
    if (dlm_name == NULL) {
        config_perror("Bad dlmod line");
        dlmod_delete_module(dlm);
        return;
    }
    strlcpy(dlm->name, dlm_name, sizeof(dlm->name));

    /*
     * dynamic module path
     */
    dlm_path = strtok_r(NULL, "\t ", &st);
    if (dlm_path)
        strlcpy(dlm->path, dlm_path, sizeof(dlm->path));
    else
        strlcpy(dlm->path, dlm_name, sizeof(dlm->path));

    dlmod_load_module(dlm);

    if (dlm->status == DLMOD_ERROR)
        snmp_log(LOG_ERR, "%s\n", dlm->error);
}

static void
dlmod_free_config(void)
{
    struct dlmod   *dtmp, *dtmp2;

    for (dtmp = dlmods; dtmp != NULL;) {
        dtmp2 = dtmp;
        dtmp = dtmp->next;
        dlmod_unload_module(dtmp2);
        free(dtmp2->error);
        free(dtmp2);
    }
    dlmods = NULL;
}

/*
 * Functions to handle SNMP management
 */

#define DLMODNEXTINDEX 		1
#define DLMODINDEX     		2
#define DLMODNAME      		3
#define DLMODPATH      		4
#define DLMODERROR     		5
#define DLMODSTATUS    		6

/*
 * header_dlmod(...
 * Arguments:
 * vp     IN      - pointer to variable entry that points here
 * name    IN/OUT  - IN/name requested, OUT/name found
 * length  IN/OUT  - length of IN/OUT oid's
 * exact   IN      - TRUE if an exact match was requested
 * var_len OUT     - length of variable or 0 if function returned
 * write_method
 */

static int
header_dlmod(struct variable *vp,
             oid * name,
             size_t * length,
             int exact, size_t * var_len, WriteMethod ** write_method)
{
#define DLMOD_NAME_LENGTH 10
    oid             newname[MAX_OID_LEN];
    int             result;

    memcpy(newname, vp->name, vp->namelen * sizeof(oid));
    newname[DLMOD_NAME_LENGTH] = 0;

    result = snmp_oid_compare(name, *length, newname, vp->namelen + 1);
    if ((exact && (result != 0)) || (!exact && (result >= 0))) {
        return MATCH_FAILED;
    }

    memcpy(name, newname, (vp->namelen + 1) * sizeof(oid));
    *length = vp->namelen + 1;
    *write_method = NULL;
    *var_len = sizeof(long);    /* default to 'long' results */
    return MATCH_SUCCEEDED;
}


static u_char         *
var_dlmod(struct variable * vp,
          oid * name,
          size_t * length,
          int exact, size_t * var_len, WriteMethod ** write_method)
{

    /*
     * variables we may use later
     */

    *write_method = NULL;      /* assume it isn't writable for the time being */
    *var_len = sizeof(int);    /* assume an integer and change later if not */

    if (header_dlmod(vp, name, length, exact,
                     var_len, write_method) == MATCH_FAILED)
        return NULL;

    /*
     * this is where we do the value assignments for the mib results.
     */
    switch (vp->magic) {
    case DLMODNEXTINDEX:
        long_return = dlmod_next_index;
        return (unsigned char *) &long_return;
    default:
        DEBUGMSGTL(("dlmod", "unknown sub-id %d in var_dlmod\n",
                    vp->magic));
    }
    return NULL;
}


static int
write_dlmodName(int action,
                u_char * var_val,
                u_char var_val_type,
                size_t var_val_len,
                u_char * statP, oid * name, size_t name_len)
{
    static struct dlmod *dlm;

    if (var_val_type != ASN_OCTET_STR) {
        snmp_log(LOG_ERR, "write to dlmodName not ASN_OCTET_STR\n");
        return SNMP_ERR_WRONGTYPE;
    }
    if (var_val_len > sizeof(dlm->name)-1) {
        snmp_log(LOG_ERR, "write to dlmodName: bad length: too long\n");
        return SNMP_ERR_WRONGLENGTH;
    }
    if (action == COMMIT) {
        dlm = dlmod_get_by_index(name[12]);
        if (!dlm || dlm->status == DLMOD_LOADED)
            return SNMP_ERR_RESOURCEUNAVAILABLE;
        strncpy(dlm->name, (const char *) var_val, var_val_len);
        dlm->name[var_val_len] = 0;
    }
    return SNMP_ERR_NOERROR;
}

static int
write_dlmodPath(int action,
                u_char * var_val,
                u_char var_val_type,
                size_t var_val_len,
                u_char * statP, oid * name, size_t name_len)
{
    static struct dlmod *dlm;

    if (var_val_type != ASN_OCTET_STR) {
        snmp_log(LOG_ERR, "write to dlmodPath not ASN_OCTET_STR\n");
        return SNMP_ERR_WRONGTYPE;
    }
    if (var_val_len > sizeof(dlm->path)-1) {
        snmp_log(LOG_ERR, "write to dlmodPath: bad length: too long\n");
        return SNMP_ERR_WRONGLENGTH;
    }
    if (action == COMMIT) {
        dlm = dlmod_get_by_index(name[12]);
        if (!dlm || dlm->status == DLMOD_LOADED)
            return SNMP_ERR_RESOURCEUNAVAILABLE;
        strncpy(dlm->path, (const char *) var_val, var_val_len);
        dlm->path[var_val_len] = 0;
    }
    return SNMP_ERR_NOERROR;
}

static int
write_dlmodStatus(int action,
                  u_char * var_val,
                  u_char var_val_type,
                  size_t var_val_len,
                  u_char * statP, oid * name, size_t name_len)
{
    /*
     * variables we may use later
     */
    struct dlmod   *dlm;

    if (var_val_type != ASN_INTEGER) {
        snmp_log(LOG_ERR, "write to dlmodStatus not ASN_INTEGER\n");
        return SNMP_ERR_WRONGTYPE;
    }
    if (var_val_len > sizeof(long)) {
        snmp_log(LOG_ERR, "write to dlmodStatus: bad length\n");
        return SNMP_ERR_WRONGLENGTH;
    }
    if (action == COMMIT) {
        /*
         * object identifier in form .1.3.6.1.4.1.2021.13.14.2.1.4.x
         * where X is index with offset 12
         */

        dlm = dlmod_get_by_index(name[12]);
        switch (*((long *) var_val)) {
        case DLMOD_CREATE:
            if (dlm || (name[12] != dlmod_next_index))
                return SNMP_ERR_RESOURCEUNAVAILABLE;
            dlm = dlmod_create_module();
            if (!dlm)
                return SNMP_ERR_RESOURCEUNAVAILABLE;
            break;
        case DLMOD_LOAD:
            if (!dlm || dlm->status == DLMOD_LOADED)
                return SNMP_ERR_RESOURCEUNAVAILABLE;
            dlmod_load_module(dlm);
            break;
        case DLMOD_UNLOAD:
            if (!dlm || dlm->status != DLMOD_LOADED)
                return SNMP_ERR_RESOURCEUNAVAILABLE;
            dlmod_unload_module(dlm);
            break;
        case DLMOD_DELETE:
            if (!dlm || dlm->status == DLMOD_LOADED)
                return SNMP_ERR_RESOURCEUNAVAILABLE;
            dlmod_delete_module(dlm);
            break;
        default:
            return SNMP_ERR_WRONGVALUE;
        }
    }
    return SNMP_ERR_NOERROR;
}

/*
 * header_dlmodEntry(...
 * Arguments:
 * vp     IN      - pointer to variable entry that points here
 * name    IN/OUT  - IN/name requested, OUT/name found
 * length  IN/OUT  - length of IN/OUT oid's
 * exact   IN      - TRUE if an exact match was requested
 * var_len OUT     - length of variable or 0 if function returned
 * write_method
 *
 */


static struct dlmod *
header_dlmodEntry(struct variable *vp,
                  oid * name,
                  size_t * length,
                  int exact, size_t * var_len, WriteMethod ** write_method)
{
#define DLMODENTRY_NAME_LENGTH 12
    oid             newname[MAX_OID_LEN];
    int             result;
    struct dlmod   *dlm = NULL;
    unsigned int    dlmod_index;

    memcpy(newname, vp->name, vp->namelen * sizeof(oid));
    *write_method = NULL;

    for (dlmod_index = 1; dlmod_index < dlmod_next_index; dlmod_index++) {
        dlm = dlmod_get_by_index(dlmod_index);

        DEBUGMSGTL(("dlmod", "dlmodEntry dlm: %p dlmod_index: %d\n",
                    dlm, dlmod_index));

        if (dlm) {
            newname[12] = dlmod_index;
            result = snmp_oid_compare(name, *length, newname, vp->namelen + 1);

            if ((exact && (result == 0)) || (!exact && (result < 0)))
                break;
        }
    }

    if (dlmod_index >= dlmod_next_index) {
        if (dlmod_index == dlmod_next_index &&
            exact && vp->magic == DLMODSTATUS)

            *write_method = write_dlmodStatus;
        return NULL;
    }

    memcpy(name, newname, (vp->namelen + 1) * sizeof(oid));
    *length = vp->namelen + 1;
    *var_len = sizeof(long);
    return dlm;
}

static u_char         *
var_dlmodEntry(struct variable * vp,
               oid * name,
               size_t * length,
               int exact, size_t * var_len, WriteMethod ** write_method)
{
    /*
     * variables we may use later
     */
    struct dlmod   *dlm;

    *var_len = sizeof(int);     /* assume an integer and change later
                                 * if not */

    dlm = header_dlmodEntry(vp, name, length, exact, var_len, write_method);
    if (dlm == NULL)
        return NULL;

    /*
     * this is where we do the value assignments for the mib results.
     */
    switch (vp->magic) {
    case DLMODNAME:
        *write_method = write_dlmodName;
        *var_len = strlen(dlm->name);
        return (unsigned char *) dlm->name;
    case DLMODPATH:
        *write_method = write_dlmodPath;
        *var_len = strlen(dlm->path);
        return (unsigned char *) dlm->path;
    case DLMODERROR:
        *var_len = dlm->error ? strlen(dlm->error) : 0;
        return (unsigned char *) dlm->error;
    case DLMODSTATUS:
        *write_method = write_dlmodStatus;
        long_return = dlm->status;
        return (unsigned char *) &long_return;
    default:
        DEBUGMSGTL(("dlmod", "unknown sub-id %d in var_dlmodEntry\n",
                    vp->magic));
    }
    return NULL;
}

/*
 * this variable defines function callbacks and type return
 * information for the dlmod mib
 */
static struct variable4 dlmod_variables[] = {
    {DLMODNEXTINDEX, ASN_INTEGER, NETSNMP_OLDAPI_RONLY,
     var_dlmod, 1, {1}},
    {DLMODNAME, ASN_OCTET_STR, NETSNMP_OLDAPI_RWRITE,
     var_dlmodEntry, 3, {2, 1, 2}},
    {DLMODPATH, ASN_OCTET_STR, NETSNMP_OLDAPI_RWRITE,
     var_dlmodEntry, 3, {2, 1, 3}},
    {DLMODERROR, ASN_OCTET_STR, NETSNMP_OLDAPI_RONLY,
     var_dlmodEntry, 3, {2, 1, 4}},
    {DLMODSTATUS, ASN_INTEGER, NETSNMP_OLDAPI_RWRITE,
     var_dlmodEntry, 3, {2, 1, 5}},
};

static oid dlmod_variables_oid[] = { 1, 3, 6, 1, 4, 1, 2021, 13, 14 };

void
init_dlmod(void)
{
    REGISTER_MIB("dlmod", dlmod_variables, variable4, dlmod_variables_oid);

    /*
     * TODO: REGISTER_SYSOR_ENTRY
     */

    DEBUGMSGTL(("dlmod", "register mib\n"));

    snmpd_register_config_handler("dlmod", dlmod_parse_config,
                                  dlmod_free_config,
                                  "module-name module-path");

    {
        const char * const p = getenv("SNMPDLMODPATH");
        strncpy(dlmod_path, SNMPDLMODPATH, sizeof(dlmod_path));
        dlmod_path[ sizeof(dlmod_path) - 1 ] = 0;
        if (p) {
            if (p[0] == ':') {
                int len = strlen(dlmod_path);
                if (dlmod_path[len - 1] != ':') {
                    strncat(dlmod_path, ":", sizeof(dlmod_path) - len - 1);
                    len++;
                }
                strncat(dlmod_path, p + 1,   sizeof(dlmod_path) - len);
            } else
                strncpy(dlmod_path, p, sizeof(dlmod_path));
        }
    }

    dlmod_path[ sizeof(dlmod_path)-1 ] = 0;
    DEBUGMSGTL(("dlmod", "dlmod_path: %s\n", dlmod_path));
}

netsnmp_feature_require(snmpd_unregister_config_handler);

void
shutdown_dlmod(void)
{
    snmpd_unregister_config_handler("dlmod");
    unregister_mib(dlmod_variables_oid, OID_LENGTH(dlmod_variables_oid));
}
