/**
 * @file imczmq.c
 * @brief Input module for receiving messages via ZeroMQ.
 *
 * The module creates one or more CZMQ sockets according to the
 * configuration and converts each incoming frame into an rsyslog
 * message processed by the specified ruleset.  Socket type, topics and
 * CURVE authentication parameters are fully configurable.
 *
 * Copyright (C) 2016 Brian Knox
 * Copyright (C) 2014 Rainer Gerhards
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *       -or-
 *     see COPYING.ASL20 in the source distribution
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "config.h"
#include "rsyslog.h"
#include <assert.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cfsysline.h"
#include "dirty.h"
#include "errmsg.h"
#include "glbl.h"
#include "module-template.h"
#include "msg.h"
#include "net.h"
#include "parser.h"
#include "prop.h"
#include "ruleset.h"
#include "srUtils.h"
#include "unicode-helper.h"
#include <czmq.h>

MODULE_TYPE_INPUT;
MODULE_TYPE_NOKEEP;
MODULE_CNFNAME("imczmq");

DEF_IMOD_STATIC_DATA;
DEFobjCurrIf(glbl) DEFobjCurrIf(prop) DEFobjCurrIf(ruleset)

    static struct cnfparamdescr modpdescr[] = {
        {"authenticator", eCmdHdlrBinary, 0},
        {"authtype", eCmdHdlrString, 0},
        {"servercertpath", eCmdHdlrString, 0},
        {"clientcertpath", eCmdHdlrString, 0},
};

static struct cnfparamblk modpblk = {CNFPARAMBLK_VERSION, sizeof(modpdescr) / sizeof(struct cnfparamdescr), modpdescr};

struct modConfData_s {
    rsconf_t *pConf;
    instanceConf_t *root;
    instanceConf_t *tail;
    int authenticator;
    char *authType;
    char *serverCertPath;
    char *clientCertPath;
};

struct instanceConf_s {
    bool serverish;
    int sockType;
    char *sockEndpoints;
    char *topics;
    uchar *pszBindRuleset;
    ruleset_t *pBindRuleset;
    struct instanceConf_s *next;
};

struct listener_t {
    zsock_t *sock;
    ruleset_t *ruleset;
};

static zlist_t *listenerList;
static modConfData_t *runModConf = NULL;
static prop_t *s_namep = NULL;
enum { IMCZMQ_MAX_TOPIC_LEN = 255 };

static struct cnfparamdescr inppdescr[] = {
    {"endpoints", eCmdHdlrGetWord, 1},
    {"socktype", eCmdHdlrGetWord, 1},
    {"ruleset", eCmdHdlrGetWord, 0},
    {"topics", eCmdHdlrGetWord, 0},
};

#include "im-helper.h"

static struct cnfparamblk inppblk = {CNFPARAMBLK_VERSION, sizeof(inppdescr) / sizeof(struct cnfparamdescr), inppdescr};

static void setDefaults(instanceConf_t *iconf) {
    iconf->serverish = true;
    iconf->sockType = -1;
    iconf->sockEndpoints = NULL;
    iconf->topics = NULL;
    iconf->pszBindRuleset = NULL;
    iconf->pBindRuleset = NULL;
    iconf->next = NULL;
};

static rsRetVal validateTopics(const char *topics) {
    DEFiRet;
    const char *cursor;

    if (topics == NULL) {
        FINALIZE;
    }

    for (cursor = topics; *cursor != '\0';) {
        const char *delimiter = strchr(cursor, ',');
        const size_t topic_len = (delimiter == NULL) ? strlen(cursor) : (size_t)(delimiter - cursor);

        if (topic_len > IMCZMQ_MAX_TOPIC_LEN) {
            LogError(0, RS_RET_PARAM_ERROR, "imczmq: configured topic exceeds maximum supported length of %u bytes",
                     (unsigned)IMCZMQ_MAX_TOPIC_LEN);
            ABORT_FINALIZE(RS_RET_PARAM_ERROR);
        }

        if (delimiter == NULL) {
            break;
        }
        cursor = delimiter + 1;
    }

finalize_it:
    RETiRet;
}

/**
 * @brief Allocate a new listener configuration block.
 *
 * The new instance is initialized with default values and appended to
 * the module's list of listeners which will be activated when the
 * input starts.
 *
 * @param[out] pinst pointer receiving the allocated instance
 * @retval RS_RET_OK on success
 * @retval RS_RET_OUT_OF_MEMORY if allocation fails
 */
static rsRetVal createInstance(instanceConf_t **pinst) {
    DEFiRet;
    instanceConf_t *inst;
    CHKmalloc(inst = malloc(sizeof(instanceConf_t)));

    setDefaults(inst);

    if (runModConf->root == NULL || runModConf->tail == NULL) {
        runModConf->tail = runModConf->root = inst;
    } else {
        runModConf->tail->next = inst;
        runModConf->tail = inst;
    }
    *pinst = inst;
finalize_it:
    RETiRet;
}

/**
 * @brief Create a CZMQ listener socket from the given configuration.
 *
 * The function initializes the socket, applies optional CURVE
 * authentication, subscribes to topics and attaches the socket to the
 * configured endpoints.  The resulting listener structure is stored in
 * the global list used by the receiver loop.
 *
 * @param iconf configuration describing one listener
 * @retval RS_RET_OK on success
 * @retval RS_RET_ERR if socket setup fails
 */
static rsRetVal addListener(instanceConf_t *iconf) {
    DEFiRet;

    DBGPRINTF("imczmq: addListener called..\n");
    struct listener_t *pData = NULL;
    CHKmalloc(pData = (struct listener_t *)malloc(sizeof(struct listener_t)));
    pData->ruleset = iconf->pBindRuleset;

    pData->sock = zsock_new(iconf->sockType);
    if (!pData->sock) {
        LogError(0, RS_RET_NO_ERRCODE, "imczmq: new socket failed for endpoints: %s", iconf->sockEndpoints);
        ABORT_FINALIZE(RS_RET_NO_ERRCODE);
    }

    DBGPRINTF("imczmq: created socket of type %d..\n", iconf->sockType);

    if (runModConf->authType) {
        if (!strcmp(runModConf->authType, "CURVESERVER")) {
            DBGPRINTF("imczmq: we are a CURVESERVER\n");
            zcert_t *serverCert = zcert_load(runModConf->serverCertPath);
            if (!serverCert) {
                LogError(0, NO_ERRCODE, "could not load cert %s", runModConf->serverCertPath);
                ABORT_FINALIZE(RS_RET_ERR);
            }
            zsock_set_zap_domain(pData->sock, "global");
            zsock_set_curve_server(pData->sock, 1);
            zcert_apply(serverCert, pData->sock);
            zcert_destroy(&serverCert);
        } else if (!strcmp(runModConf->authType, "CURVECLIENT")) {
            DBGPRINTF("imczmq: we are a CURVECLIENT\n");
            zcert_t *serverCert = zcert_load(runModConf->serverCertPath);
            if (!serverCert) {
                LogError(0, NO_ERRCODE, "could not load cert %s", runModConf->serverCertPath);
                ABORT_FINALIZE(RS_RET_ERR);
            }
            const char *server_key = zcert_public_txt(serverCert);
            zcert_destroy(&serverCert);
            zsock_set_curve_serverkey(pData->sock, server_key);

            zcert_t *clientCert = zcert_load(runModConf->clientCertPath);
            if (!clientCert) {
                LogError(0, NO_ERRCODE, "could not load cert %s", runModConf->clientCertPath);
                ABORT_FINALIZE(RS_RET_ERR);
            }

            zcert_apply(clientCert, pData->sock);
            zcert_destroy(&clientCert);
        }
    }

    switch (iconf->sockType) {
        case ZMQ_SUB:
/* ZMQ_DISH socket type is available and stable in all czmq versions that
 * define it. The previous version restriction (< 4.2.0) was incorrect and
 * has been removed. */
#if defined(ZMQ_DISH)
        case ZMQ_DISH:
#endif
            iconf->serverish = false;
            break;
        case ZMQ_PULL:
#if defined(ZMQ_GATHER)
        case ZMQ_GATHER:
#endif
        case ZMQ_ROUTER:
#if defined(ZMQ_SERVER)
        case ZMQ_SERVER:
#endif
            iconf->serverish = true;
            break;
        default:
            break;
    }

    if (iconf->topics) {
        const char *topic_cursor = iconf->topics;
        // A zero-length topic means subscribe to everything
        if (!*topic_cursor && iconf->sockType == ZMQ_SUB) {
            DBGPRINTF("imczmq: subscribing to all topics\n");
            zsock_set_subscribe(pData->sock, "");
        }

        char topic[IMCZMQ_MAX_TOPIC_LEN + 1];
        while (*topic_cursor) {
            const char *delimiter = strchr(topic_cursor, ',');
            const size_t topic_len = (delimiter == NULL) ? strlen(topic_cursor) : (size_t)(delimiter - topic_cursor);
            if (!delimiter) {
                delimiter = topic_cursor + topic_len;
            }
            memcpy(topic, topic_cursor, topic_len);
            topic[topic_len] = 0;
            DBGPRINTF("imczmq: subscribing to %s\n", topic);
            if (iconf->sockType == ZMQ_SUB) {
                zsock_set_subscribe(pData->sock, topic);
            }
/* ZMQ_DISH socket type support - available in all czmq versions that define it */
#if defined(ZMQ_DISH)
            else if (iconf->sockType == ZMQ_DISH) {
                int rc = zsock_join(pData->sock, topic);
                if (rc != 0) {
                    LogError(0, NO_ERRCODE, "could not join group %s", topic);
                    ABORT_FINALIZE(RS_RET_ERR);
                }
            }
#endif
            if (*delimiter == 0) {
                break;
            }
            topic_cursor = delimiter + 1;
        }
    }

    int rc = zsock_attach(pData->sock, (const char *)iconf->sockEndpoints, iconf->serverish);
    if (rc == -1) {
        LogError(0, NO_ERRCODE, "zsock_attach to %s failed", iconf->sockEndpoints);
        ABORT_FINALIZE(RS_RET_ERR);
    }

    DBGPRINTF("imczmq: attached socket to %s\n", iconf->sockEndpoints);

    rc = zlist_append(listenerList, (void *)pData);
    if (rc != 0) {
        LogError(0, NO_ERRCODE, "could not append listener");
        ABORT_FINALIZE(RS_RET_ERR);
    }
finalize_it:
    if (iRet != RS_RET_OK) {
        free(pData);
    }
    RETiRet;
}

/**
 * @brief Receive messages from all configured CZMQ listeners.
 *
 * Builds a poller for the active sockets, waits for incoming frames and
 * converts them into rsyslog messages which are then processed by the
 * listener's ruleset.  Authentication actors are started when requested
 * and resources are cleaned up on termination.
 *
 * @retval RS_RET_OK on graceful shutdown
 * @retval RS_RET_ERR on initialization or runtime failure
 */
static rsRetVal rcvData(void) {
    DEFiRet;

    if (!listenerList) {
        listenerList = zlist_new();
        if (!listenerList) {
            LogError(0, NO_ERRCODE, "could not allocate list");
            ABORT_FINALIZE(RS_RET_ERR);
        }
    }

    zactor_t *authActor = NULL;

    if (runModConf->authenticator == 1) {
        authActor = zactor_new(zauth, NULL);
        zstr_sendx(authActor, "CURVE", runModConf->clientCertPath, NULL);
        zsock_wait(authActor);
    }

    instanceConf_t *inst;
    for (inst = runModConf->root; inst != NULL; inst = inst->next) {
        CHKiRet(addListener(inst));
    }

    zpoller_t *poller = zpoller_new(NULL);
    if (!poller) {
        LogError(0, NO_ERRCODE, "could not create poller");
        ABORT_FINALIZE(RS_RET_ERR);
    }
    DBGPRINTF("imczmq: created poller\n");

    struct listener_t *pData;

    pData = zlist_first(listenerList);
    if (!pData) {
        LogError(0, NO_ERRCODE,
                 "imczmq: no listeners were "
                 "started, input not activated.\n");
        ABORT_FINALIZE(RS_RET_NO_RUN);
    }

    while (pData) {
        int rc = zpoller_add(poller, pData->sock);
        if (rc != 0) {
            LogError(0, NO_ERRCODE,
                     "imczmq: could not add "
                     "socket to poller, input not activated.\n");
            ABORT_FINALIZE(RS_RET_NO_RUN);
        }
        pData = zlist_next(listenerList);
    }

    zsock_t *which = (zsock_t *)zpoller_wait(poller, -1);
    while (which) {
        if (zpoller_terminated(poller)) {
            break;
        }
        pData = zlist_first(listenerList);
        while (pData->sock != which) {
            pData = zlist_next(listenerList);
        }

        if (which == pData->sock) {
            DBGPRINTF("imczmq: found matching socket\n");
        }

        zframe_t *frame = zframe_recv(which);
        char *buf = NULL;

        if (frame != NULL) buf = zframe_strdup(frame);

        zframe_destroy(&frame);

        if (buf == NULL) {
            DBGPRINTF("imczmq: null buffer\n");
            continue;
        }
        smsg_t *pMsg;
        if (msgConstruct(&pMsg) == RS_RET_OK) {
            MsgSetRawMsg(pMsg, buf, strlen(buf));
            MsgSetInputName(pMsg, s_namep);
            MsgSetHOSTNAME(pMsg, glbl.GetLocalHostName(), ustrlen(glbl.GetLocalHostName()));
            MsgSetRcvFrom(pMsg, glbl.GetLocalHostNameProp());
            MsgSetRcvFromIP(pMsg, glbl.GetLocalHostIP());
            MsgSetMSGoffs(pMsg, 0);
            MsgSetFlowControlType(pMsg, eFLOWCTL_NO_DELAY);
            MsgSetRuleset(pMsg, pData->ruleset);
            pMsg->msgFlags = NEEDS_PARSING | PARSE_HOSTNAME;
            submitMsg2(pMsg);
        }

        free(buf);
        which = (zsock_t *)zpoller_wait(poller, -1);
    }
finalize_it:
    zpoller_destroy(&poller);
    pData = zlist_first(listenerList);
    while (pData) {
        zsock_destroy(&pData->sock);
        free(pData->ruleset);
        pData = zlist_next(listenerList);
    }
    zlist_destroy(&listenerList);
    zactor_destroy(&authActor);
    RETiRet;
}

BEGINrunInput
    CODESTARTrunInput;
    iRet = rcvData();
ENDrunInput


BEGINwillRun
    CODESTARTwillRun;
    CHKiRet(prop.Construct(&s_namep));
    CHKiRet(prop.SetString(s_namep, UCHAR_CONSTANT("imczmq"), sizeof("imczmq") - 1));

    CHKiRet(prop.ConstructFinalize(s_namep));

finalize_it:
ENDwillRun


BEGINafterRun
    CODESTARTafterRun;
    if (s_namep != NULL) {
        prop.Destruct(&s_namep);
    }
ENDafterRun


BEGINmodExit
    CODESTARTmodExit;
    objRelease(glbl, CORE_COMPONENT);
    objRelease(prop, CORE_COMPONENT);
    objRelease(ruleset, CORE_COMPONENT);
ENDmodExit


BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
    if (eFeat == sFEATURENonCancelInputTermination) {
        iRet = RS_RET_OK;
    }
ENDisCompatibleWithFeature


BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    runModConf = pModConf;
    runModConf->pConf = pConf;
    runModConf->authenticator = 0;
    runModConf->authType = NULL;
    runModConf->serverCertPath = NULL;
    runModConf->clientCertPath = NULL;
ENDbeginCnfLoad


BEGINsetModCnf
    struct cnfparamvals *pvals = NULL;
    int i;
    CODESTARTsetModCnf;
    pvals = nvlstGetParams(lst, &modpblk, NULL);
    if (NULL == pvals) {
        LogError(0, RS_RET_MISSING_CNFPARAMS,
                 "imczmq: error processing module "
                 "config parameters ['module(...)']");

        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    for (i = 0; i < modpblk.nParams; ++i) {
        if (!pvals[i].bUsed) {
            continue;
        }
        if (!strcmp(modpblk.descr[i].name, "authenticator")) {
            runModConf->authenticator = (int)pvals[i].val.d.n;
        } else if (!strcmp(modpblk.descr[i].name, "authtype")) {
            runModConf->authType = es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(modpblk.descr[i].name, "servercertpath")) {
            runModConf->serverCertPath = es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(modpblk.descr[i].name, "clientcertpath")) {
            runModConf->clientCertPath = es_str2cstr(pvals[i].val.d.estr, NULL);
        } else {
            LogError(0, RS_RET_INVALID_PARAMS,
                     "imczmq: config error, unknown "
                     "param %s in setModCnf\n",
                     modpblk.descr[i].name);
        }
    }

    DBGPRINTF("imczmq: authenticator set to %d\n", runModConf->authenticator);
    DBGPRINTF("imczmq: authType set to %s\n", runModConf->authType);
    DBGPRINTF("imczmq: serverCertPath set to %s\n", runModConf->serverCertPath);
    DBGPRINTF("imczmq: clientCertPath set to %s\n", runModConf->clientCertPath);

finalize_it:
    if (pvals != NULL) {
        cnfparamvalsDestruct(pvals, &modpblk);
    }
ENDsetModCnf


BEGINendCnfLoad
    CODESTARTendCnfLoad;
ENDendCnfLoad


static inline void std_checkRuleset_genErrMsg(__attribute__((unused)) modConfData_t *modConf, instanceConf_t *inst) {
    LogError(0, NO_ERRCODE,
             "imczmq: ruleset '%s' for socket %s not found - "
             "using default ruleset instead",
             inst->pszBindRuleset, inst->sockEndpoints);
}


BEGINcheckCnf
    instanceConf_t *inst;
    CODESTARTcheckCnf;
    for (inst = pModConf->root; inst != NULL; inst = inst->next) {
        std_checkRuleset(pModConf, inst);
    }
ENDcheckCnf


BEGINactivateCnfPrePrivDrop
    CODESTARTactivateCnfPrePrivDrop;
    runModConf = pModConf;
    putenv((char *)"ZSYS_SIGHANDLER=false");
ENDactivateCnfPrePrivDrop


BEGINactivateCnf
    CODESTARTactivateCnf;
ENDactivateCnf


BEGINfreeCnf
    instanceConf_t *inst, *inst_r;
    CODESTARTfreeCnf;
    free(pModConf->authType);
    free(pModConf->serverCertPath);
    free(pModConf->clientCertPath);
    for (inst = pModConf->root; inst != NULL;) {
        free(inst->pszBindRuleset);
        free(inst->sockEndpoints);
        free(inst->topics);
        inst_r = inst;
        inst = inst->next;
        free(inst_r);
    }

ENDfreeCnf


BEGINnewInpInst
    struct cnfparamvals *pvals;
    instanceConf_t *inst;
    int i;
    CODESTARTnewInpInst;
    DBGPRINTF("newInpInst (imczmq)\n");

    pvals = nvlstGetParams(lst, &inppblk, NULL);
    if (NULL == pvals) {
        LogError(0, RS_RET_MISSING_CNFPARAMS, "imczmq: required parameters are missing\n");
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    if (Debug) {
        DBGPRINTF("imczmq: input param blk:\n");
        cnfparamsPrint(&inppblk, pvals);
    }

    CHKiRet(createInstance(&inst));

    for (i = 0; i < inppblk.nParams; ++i) {
        if (!pvals[i].bUsed) {
            continue;
        }

        if (!strcmp(inppblk.descr[i].name, "ruleset")) {
            CHKmalloc(inst->pszBindRuleset = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(inppblk.descr[i].name, "endpoints")) {
            CHKmalloc(inst->sockEndpoints = es_str2cstr(pvals[i].val.d.estr, NULL));
        } else if (!strcmp(inppblk.descr[i].name, "topics")) {
            CHKmalloc(inst->topics = es_str2cstr(pvals[i].val.d.estr, NULL));
            CHKiRet(validateTopics(inst->topics));
        } else if (!strcmp(inppblk.descr[i].name, "socktype")) {
            char *stringType = es_str2cstr(pvals[i].val.d.estr, NULL);
            if (NULL == stringType) {
                LogError(0, RS_RET_CONFIG_ERROR, "imczmq: out of memory error copying sockType param");
                ABORT_FINALIZE(RS_RET_OUT_OF_MEMORY);
            }

            if (!strcmp("PULL", stringType)) {
                inst->sockType = ZMQ_PULL;
            }
#if defined(ZMQ_GATHER)
            else if (!strcmp("GATHER", stringType)) {
                inst->sockType = ZMQ_GATHER;
            }
#endif
            else if (!strcmp("SUB", stringType)) {
                inst->sockType = ZMQ_SUB;
            }
/* ZMQ_DISH socket type support - available in all czmq versions that define it */
#if defined(ZMQ_DISH)
            else if (!strcmp("DISH", stringType)) {
                inst->sockType = ZMQ_DISH;
            }
#endif
            else if (!strcmp("ROUTER", stringType)) {
                inst->sockType = ZMQ_ROUTER;
            }
#if defined(ZMQ_SERVER)
            else if (!strcmp("SERVER", stringType)) {
                inst->sockType = ZMQ_SERVER;
            }
#endif
            else {
                /* Config parsing should never accept unknown socktypes; fail closed if it does. */
                LogError(0, RS_RET_CONFIG_ERROR, "imczmq: invalid socktype '%s'", stringType);
                free(stringType);
                ABORT_FINALIZE(RS_RET_CONFIG_ERROR);
            }
            free(stringType);

        } else {
            LogError(0, NO_ERRCODE,
                     "imczmq: program error, non-handled "
                     "param '%s'\n",
                     inppblk.descr[i].name);
        }
    }
finalize_it:
    CODE_STD_FINALIZERnewInpInst cnfparamvalsDestruct(pvals, &inppblk);
ENDnewInpInst


BEGINqueryEtryPt
    CODESTARTqueryEtryPt;
    CODEqueryEtryPt_STD_IMOD_QUERIES;
    CODEqueryEtryPt_STD_CONF2_QUERIES;
    CODEqueryEtryPt_STD_CONF2_setModCnf_QUERIES;
    CODEqueryEtryPt_STD_CONF2_PREPRIVDROP_QUERIES;
    CODEqueryEtryPt_STD_CONF2_IMOD_QUERIES;
    CODEqueryEtryPt_IsCompatibleWithFeature_IF_OMOD_QUERIES;
ENDqueryEtryPt


BEGINmodInit()
    CODESTARTmodInit;
    *ipIFVersProvided = CURR_MOD_IF_VERSION;
    CODEmodInit_QueryRegCFSLineHdlr CHKiRet(objUse(glbl, CORE_COMPONENT));
    CHKiRet(objUse(prop, CORE_COMPONENT));
    CHKiRet(objUse(ruleset, CORE_COMPONENT));
ENDmodInit
