/**
 * @file omusrmsg.c
 * @brief Implementation of the built-in user message output module.
 *
 * This module provides message delivery to logged-in users.  Depending on
 * configuration it can either send messages to specific user terminals or
 * broadcast them using a "wall" style interface.
 *
 * NOTE: read comments in module-template.h to understand how this file works!
 *
 * File begun on 2007-07-20 by Rainer Gerhards (extracted from syslogd.c,
 * which at the time of the fork from sysklogd was under BSD license).
 *
 * Copyright 2007-2026 Adiscon GmbH.
 *
 * This file is part of rsyslog.
 *
 * 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 <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <signal.h>
#include <ctype.h>
#include <sys/param.h>
#ifdef HAVE_UTMPX_H
    #include <utmpx.h>
    #define STRUCTUTMP struct utmpx
    #define UTNAME ut_user
    #define setutent setutxent
    #define getutent getutxent
    #define endutent endutxent
    #ifndef UT_LINESIZE
        /* __UT_LINESIZE for glibc; _UTX_LINESIZE common on Solaris */
        #ifdef __UT_LINESIZE
            #define UT_LINESIZE __UT_LINESIZE
        #elif defined(_UTX_LINESIZE)
            #define UT_LINESIZE _UTX_LINESIZE
        #else /* method of last resort */
            #define UT_LINESIZE 32
        #endif
    #endif
#elif defined(HAVE_UTMP_H)
    #include <utmp.h>
    #define STRUCTUTMP struct utmp
    #define UTNAME ut_name
#endif
#include <unistd.h>
#include <sys/uio.h>
#include <sys/stat.h>
#include <errno.h>
#if HAVE_FCNTL_H
    #include <fcntl.h>
#else
    #include <sys/msgbuf.h>
#endif
#ifdef HAVE_PATHS_H
    #include <paths.h>
#endif
#ifndef _PATH_UTMP
    #define _PATH_UTMP "/var/run/utmp"
#endif
#ifdef HAVE_LIBSYSTEMD
    #include <systemd/sd-daemon.h>
    #include <systemd/sd-login.h>
    #include <pwd.h>
#endif
#include "rsyslog.h"
#include "srUtils.h"
#include "stringbuf.h"
#include "syslogd-types.h"
#include "conf.h"
#include "omusrmsg.h"
#include "module-template.h"
#include "errmsg.h"
#include "ratelimit.h"


/* portability: */
#ifndef _PATH_DEV
    #define _PATH_DEV "/dev/"
#endif

#ifdef UT_NAMESIZE
    #define UNAMESZ UT_NAMESIZE /* length of a login name */
#else
    #define UNAMESZ 32 /* length of a login name, 32 seems current (2018) good bet */
#endif
#define MAXUNAMES 20 /* maximum number of user names */

#ifdef OS_SOLARIS
    #pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif

MODULE_TYPE_OUTPUT;
MODULE_TYPE_NOKEEP;
MODULE_CNFNAME("omusrmsg")

/* internal structures
 */
DEF_OMOD_STATIC_DATA;

typedef struct _instanceData {
    int bIsWall; /* 1- is wall, 0 - individual users */
    char uname[MAXUNAMES][UNAMESZ + 1];
    uchar *tplName;
    int ratelimitInterval;
    int ratelimitBurst;
    uchar *pszRatelimitName;
    ratelimit_t *ratelimiter;
} instanceData;

typedef struct wrkrInstanceData {
    instanceData *pData;
} wrkrInstanceData_t;

struct modConfData_s {
    rsconf_t *pConf;
};
static modConfData_t *loadModConf = NULL;
static modConfData_t *runModConf = NULL;

typedef struct configSettings_s {
    EMPTY_STRUCT
} configSettings_t;
static configSettings_t __attribute__((unused)) cs;


/* tables for interfacing with the v6 config system */
/* action (instance) parameters */
static struct cnfparamdescr actpdescr[] = {{"users", eCmdHdlrString, CNFPARAM_REQUIRED},
                                           {"template", eCmdHdlrGetWord, 0},
                                           {"ratelimit.interval", eCmdHdlrInt, 0},
                                           {"ratelimit.burst", eCmdHdlrInt, 0},
                                           {"ratelimit.name", eCmdHdlrString, 0}};
static struct cnfparamblk actpblk = {CNFPARAMBLK_VERSION, sizeof(actpdescr) / sizeof(struct cnfparamdescr), actpdescr};

BEGINinitConfVars /* (re)set config variables to default values */
    CODESTARTinitConfVars;
ENDinitConfVars


BEGINbeginCnfLoad
    CODESTARTbeginCnfLoad;
    loadModConf = pModConf;
    pModConf->pConf = pConf;
ENDbeginCnfLoad


BEGINendCnfLoad
    CODESTARTendCnfLoad;
    loadModConf = NULL;
ENDendCnfLoad


BEGINcheckCnf
    CODESTARTcheckCnf;
ENDcheckCnf


BEGINactivateCnf
    CODESTARTactivateCnf;
    runModConf = pModConf;
ENDactivateCnf


BEGINfreeCnf
    CODESTARTfreeCnf;
ENDfreeCnf


BEGINcreateInstance
    CODESTARTcreateInstance;
ENDcreateInstance


BEGINcreateWrkrInstance
    CODESTARTcreateWrkrInstance;
ENDcreateWrkrInstance


BEGINisCompatibleWithFeature
    CODESTARTisCompatibleWithFeature;
    if (eFeat == sFEATURERepeatedMsgReduction) iRet = RS_RET_OK;
ENDisCompatibleWithFeature


BEGINfreeInstance
    CODESTARTfreeInstance;
    free(pData->tplName);
    free(pData->pszRatelimitName);
    if (pData->ratelimiter != NULL) {
        ratelimitDestruct(pData->ratelimiter);
        pData->ratelimiter = NULL;
    }
ENDfreeInstance


BEGINfreeWrkrInstance
    CODESTARTfreeWrkrInstance;
ENDfreeWrkrInstance


BEGINdbgPrintInstInfo
    register int i;
    CODESTARTdbgPrintInstInfo;
    for (i = 0; i < MAXUNAMES && *pData->uname[i]; i++) dbgprintf("%s, ", pData->uname[i]);
ENDdbgPrintInstInfo


/**
 * BSD setutent/getutent() replacement routines
 * The following routines emulate setutent() and getutent() under
 * BSD because they are not available there. We only emulate what we actually
 * need! rgerhards 2005-03-18
 */
#ifdef OS_BSD
    /* Since version 900007, FreeBSD has a POSIX compliant <utmpx.h> */
    #if defined(__FreeBSD__) && (__FreeBSD_version >= 900007)
        #ifndef setutent
            #define setutent() setutxent()
        #endif
        #ifndef getutent
            #define getutent() getutxent()
        #endif
        #ifndef endutent
            #define endutent() endutxent()
        #endif
    #else
static FILE *BSD_uf = NULL;
void setutent(void) {
    assert(BSD_uf == NULL);
    if ((BSD_uf = fopen(_PATH_UTMP, "r")) == NULL) {
        LogError(errno, NO_ERRCODE, "error opening utmp %s", _PATH_UTMP);
        return;
    }
}

STRUCTUTMP *getutent(void) {
    static STRUCTUTMP st_utmp;

    if (fread((char *)&st_utmp, sizeof(st_utmp), 1, BSD_uf) != 1) return NULL;

    return (&st_utmp);
}

void endutent(void) {
    fclose(BSD_uf);
    BSD_uf = NULL;
}
    #endif /* if defined(__FreeBSD__) */
#endif /* #ifdef OS_BSD */


/**
 * Send a message to a specific terminal device.
 *
 * The function builds the full device path from the supplied tty name
 * and writes the message using non-blocking I/O so that a blocked
 * terminal does not stall the caller.
 *
 * @param tty  terminal name (e.g. "pts/0")
 * @param pMsg message text to send
 */
static void sendwallmsg(const char *tty, uchar *pMsg) {
    uchar szErr[512];
    int errnoSave;
    char p[sizeof(_PATH_DEV) + UT_LINESIZE];
    int ttyf;
    struct stat statb;
    int wrRet;

    /* compute the device name */
    RS_COPY_LITERAL(p, _PATH_DEV);
    size_t base_len = strlen(p);
    size_t avail = sizeof(p) - base_len - 1;
    size_t ttylen = strnlen(tty, avail);
    memcpy(p + base_len, tty, ttylen);
    p[base_len + ttylen] = '\0';

    /* we must be careful when writing to the terminal. A terminal may block
     * (for example, a user has pressed <ctl>-s). In that case, we can not
     * wait indefinitely. So we need to use non-blocking I/O. In case we would
     * block, we simply do not send the message, because that's the best we can
     * do. -- rgerhards, 2008-07-04
     */

    /* open the terminal */
    if ((ttyf = open(p, O_WRONLY | O_NOCTTY | O_NONBLOCK)) >= 0) {
        if (fstat(ttyf, &statb) == 0 && (statb.st_mode & S_IWRITE)) {
            wrRet = write(ttyf, pMsg, strlen((char *)pMsg));
            if (Debug && wrRet == -1) {
                /* we record the state to the debug log */
                errnoSave = errno;
                rs_strerror_r(errno, (char *)szErr, sizeof(szErr));
                dbgprintf("write to terminal '%s' failed with [%d]:%s\n", p, errnoSave, szErr);
            }
        }
        close(ttyf);
    }
}

/**
 * Write a message to the world at large.
 *
 * The message can either be broadcast to all logged-in users or sent only
 * to the set configured in @a pData. The implementation avoids forking a
 * helper process and instead performs delivery on the caller's thread.
 *
 * @param[in] pMsg   message text to deliver
 * @param[in] pData  per-action configuration
 *
 * @return rsRetVal
 */
static rsRetVal wallmsg(uchar *pMsg, instanceData *pData) {
    register int i;
    STRUCTUTMP ut;
    STRUCTUTMP *uptr;
    DEFiRet;

    assert(pMsg != NULL);

#ifdef HAVE_LIBSYSTEMD
    if (sd_booted() > 0) {
        register int j;
        int sdRet;
        char **sessions_list;
        int sessions = sd_get_sessions(&sessions_list);

        for (j = 0; j < sessions; j++) {
            uchar szErr[512];
            char *tty;
            const char *user = NULL;
            uid_t uid;
            struct passwd *pws;

            sdRet = sd_session_get_uid(sessions_list[j], &uid);
            if (sdRet >= 0) {
                pws = getpwuid(uid);

                if (pws == NULL) {
                    LogError(0, NO_ERRCODE, "failed to get passwd for userid '%d'\n", uid);
                    free(sessions_list[j]);
                    continue;
                }

                user = pws->pw_name; /* DO NOT FREE, OS/LIB internal memory! */

                if (user == NULL) {
                    LogError(0, NO_ERRCODE, "failed to get username for userid '%d'\n", uid);
                    free(sessions_list[j]);
                    continue;
                }
            } else {
                /* we record the state to the debug log */
                rs_strerror_r(-sdRet, (char *)szErr, sizeof(szErr));
                LogError(0, NO_ERRCODE, "get userid for session '%s' failed with [%d]:%s\n", sessions_list[j], -sdRet,
                         szErr);
                free(sessions_list[j]);
                continue; /* try next session */
            }
            /* should we send the message to this user? */
            if (pData->bIsWall == 0) {
                for (i = 0; i < MAXUNAMES; i++) {
                    if (!pData->uname[i][0]) {
                        i = MAXUNAMES;
                        break;
                    }
                    if (strncmp(pData->uname[i], user, UNAMESZ) == 0) break;
                }
                if (i == MAXUNAMES) { /* user not found? */
                    free(sessions_list[j]);
                    continue; /* on to next user! */
                }
            }
            if ((sdRet = sd_session_get_tty(sessions_list[j], &tty)) < 0) {
                /* we record the state to the debug log */
                rs_strerror_r(-sdRet, (char *)szErr, sizeof(szErr));
                LogError(0, NO_ERRCODE, "get tty for session '%s' failed with [%d]:%s\n", sessions_list[j], -sdRet,
                         szErr);
                free(sessions_list[j]);
                continue; /* try next session */
            }

            sendwallmsg(tty, pMsg);

            free(tty);
            free(sessions_list[j]);
        }
        free(sessions_list);
    } else {
#endif

        /* open the user login file */
        setutent();

        /* scan the user login file */
        while ((uptr = getutent())) {
            memcpy(&ut, uptr, sizeof(ut));
            /* is this slot used? */
            if (ut.UTNAME[0] == '\0') continue;
#ifndef OS_BSD
            if (ut.ut_type != USER_PROCESS) continue;
#endif
            if (!(memcmp(ut.UTNAME, "LOGIN", 6))) /* paranoia */
                continue;

            /* should we send the message to this user? */
            if (pData->bIsWall == 0) {
                for (i = 0; i < MAXUNAMES; i++) {
                    if (!pData->uname[i][0]) {
                        i = MAXUNAMES;
                        break;
                    }
                    if (strncmp(pData->uname[i], ut.UTNAME, UNAMESZ) == 0) break;
                }
                if (i == MAXUNAMES) /* user not found? */
                    continue; /* on to next user! */
            }

            sendwallmsg(ut.ut_line, pMsg);
        }

        /* close the user login file */
        endutent();
#ifdef HAVE_LIBSYSTEMD
    }
#endif
    RETiRet;
}


BEGINtryResume
    CODESTARTtryResume;
ENDtryResume

BEGINdoAction
    CODESTARTdoAction;
    dbgprintf("\n");
    if (pWrkrData->pData->ratelimiter == NULL ||
        ratelimitMsgCount(pWrkrData->pData->ratelimiter, 0, "omusrmsg") != RS_RET_DISCARDMSG) {
        iRet = wallmsg(ppString[0], pWrkrData->pData);
    }
ENDdoAction


static void
/**
 * Parse a comma separated list of usernames.
 *
 * The user list is copied into the instance data. Whitespace is skipped
 * and overly long names are reported via the error log. If more than
 * MAXUNAMES users are specified, excess entries are ignored.
 *
 * @param[in] pData Instance configuration to populate
 * @param[in] usrs  comma separated list of users
 */
populateUsers(instanceData *pData, es_str_t *usrs)
{
    int i;
    int iDst;
    es_size_t iUsr;
    es_size_t len;
    uchar *c;

    len = es_strlen(usrs);
    c = es_getBufAddr(usrs);
    pData->bIsWall = 0; /* write to individual users */
    iUsr = 0;
    for (i = 0; i < MAXUNAMES && iUsr < len; ++i) {
        for (iDst = 0; iDst < UNAMESZ && iUsr < len && c[iUsr] != ','; ++iDst, ++iUsr) {
            pData->uname[i][iDst] = c[iUsr];
        }
        pData->uname[i][iDst] = '\0';
        DBGPRINTF("omusrmsg: send to user '%s'\n", pData->uname[i]);
        if (iUsr < len && c[iUsr] != ',') {
            LogError(0, RS_RET_ERR,
                     "user name '%s...' too long - "
                     "ignored",
                     pData->uname[i]);
            --i;
            ++iUsr;
            while (iUsr < len && c[iUsr] != ',') ++iUsr; /* skip to next name */
        } else if (iDst == 0) {
            LogError(0, RS_RET_ERR,
                     "no user name given - "
                     "ignored");
            --i;
            ++iUsr;
            while (iUsr < len && c[iUsr] != ',') ++iUsr; /* skip to next name */
        }
        if (iUsr < len) {
            ++iUsr; /* skip "," */
            while (iUsr < len && isspace(c[iUsr])) ++iUsr; /* skip whitespace */
        }
    }
    if (i == MAXUNAMES && iUsr != len) {
        LogError(0, RS_RET_ERR,
                 "omusrmsg supports only up to %d "
                 "user names in a single action - all others have been ignored",
                 MAXUNAMES);
    }
}


static inline void setInstParamDefaults(instanceData *pData) {
    pData->bIsWall = 0;
    pData->tplName = NULL;
    pData->ratelimiter = NULL;
    pData->ratelimitInterval = -1;
    pData->ratelimitBurst = -1;
    pData->pszRatelimitName = NULL;
}

BEGINnewActInst
    struct cnfparamvals *pvals;
    int i;
    CODESTARTnewActInst;
    if ((pvals = nvlstGetParams(lst, &actpblk, NULL)) == NULL) {
        ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS);
    }

    CHKiRet(createInstance(&pData));
    setInstParamDefaults(pData);

    CODE_STD_STRING_REQUESTnewActInst(1);
    for (i = 0; i < actpblk.nParams; ++i) {
        if (!pvals[i].bUsed) continue;
        if (!strcmp(actpblk.descr[i].name, "users")) {
            if (!es_strbufcmp(pvals[i].val.d.estr, (uchar *)"*", 1)) {
                pData->bIsWall = 1;
            } else {
                populateUsers(pData, pvals[i].val.d.estr);
            }
        } else if (!strcmp(actpblk.descr[i].name, "template")) {
            pData->tplName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else if (!strcmp(actpblk.descr[i].name, "ratelimit.interval")) {
            pData->ratelimitInterval = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "ratelimit.burst")) {
            pData->ratelimitBurst = (int)pvals[i].val.d.n;
        } else if (!strcmp(actpblk.descr[i].name, "ratelimit.name")) {
            pData->pszRatelimitName = (uchar *)es_str2cstr(pvals[i].val.d.estr, NULL);
        } else {
            dbgprintf(
                "omusrmsg: program error, non-handled "
                "param '%s'\n",
                actpblk.descr[i].name);
        }
    }

    if (pData->pszRatelimitName != NULL) {
        if (pData->ratelimitInterval != -1 || pData->ratelimitBurst != -1) {
            LogError(0, RS_RET_INVALID_PARAMS,
                     "omusrmsg: ratelimit.name is mutually exclusive with "
                     "ratelimit.interval and ratelimit.burst - using ratelimit.name");
        }
        pData->ratelimitInterval = 0;
        pData->ratelimitBurst = 0;
    } else {
        if (pData->ratelimitInterval == -1) pData->ratelimitInterval = 0;
        if (pData->ratelimitBurst == -1) pData->ratelimitBurst = 200;
    }

    if (pData->tplName == NULL) {
        CHKiRet(OMSRsetEntry(*ppOMSR, 0, (uchar *)strdup(pData->bIsWall ? " WallFmt" : " StdUsrMsgFmt"),
                             OMSR_NO_RQD_TPL_OPTS));
    } else {
        CHKiRet(OMSRsetEntry(*ppOMSR, 0, (uchar *)strdup((char *)pData->tplName), OMSR_NO_RQD_TPL_OPTS));
    }

    if (pData->pszRatelimitName != NULL) {
        CHKiRet(ratelimitNewFromConfig(&pData->ratelimiter, loadModConf->pConf, (char *)pData->pszRatelimitName,
                                       "omusrmsg", NULL));
        ratelimitSetNoTimeCache(pData->ratelimiter);
    } else if (pData->ratelimitInterval > 0) {
        CHKiRet(ratelimitNew(&pData->ratelimiter, "omusrmsg", NULL));
        ratelimitSetLinuxLike(pData->ratelimiter, (unsigned)pData->ratelimitInterval, (unsigned)pData->ratelimitBurst);
        ratelimitSetNoTimeCache(pData->ratelimiter);
    }

    CODE_STD_FINALIZERnewActInst;
    cnfparamvalsDestruct(pvals, &actpblk);
ENDnewActInst


BEGINparseSelectorAct
    es_str_t *usrs;
    int bHadWarning;
    CODESTARTparseSelectorAct;
    CODE_STD_STRING_REQUESTparseSelectorAct(1) bHadWarning = 0;
    if (!strncmp((char *)p, ":omusrmsg:", sizeof(":omusrmsg:") - 1)) {
        p += sizeof(":omusrmsg:") - 1; /* eat indicator sequence  (-1 because of '\0'!) */
    } else {
        if (!*p || !((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_' ||
                     *p == '.' || *p == '*')) {
            ABORT_FINALIZE(RS_RET_CONFLINE_UNPROCESSED);
        } else {
            LogMsg(0, RS_RET_OUTDATED_STMT, LOG_WARNING,
                   "action '%s' treated as ':omusrmsg:%s' - please "
                   "use ':omusrmsg:%s' syntax instead, '%s' will "
                   "not be supported in the future",
                   p, p, p, p);
            bHadWarning = 1;
        }
    }

    CHKiRet(createInstance(&pData));

    if (*p == '*') { /* wall */
        dbgprintf("write-all");
        ++p; /* eat '*' */
        pData->bIsWall = 1; /* write to all users */
        CHKiRet(cflineParseTemplateName(&p, *ppOMSR, 0, OMSR_NO_RQD_TPL_OPTS, (uchar *)" WallFmt"));
    } else {
        /* everything else is currently treated as a user name */
        usrs = es_newStr(128);
        while (*p && *p != ';') {
            es_addChar(&usrs, *p);
            ++p;
        }
        populateUsers(pData, usrs);
        es_deleteStr(usrs);
        if ((iRet = cflineParseTemplateName(&p, *ppOMSR, 0, OMSR_NO_RQD_TPL_OPTS, (uchar *)" StdUsrMsgFmt")) !=
            RS_RET_OK)
            goto finalize_it;
    }
    if (iRet == RS_RET_OK && bHadWarning) iRet = RS_RET_OK_WARN;
    CODE_STD_FINALIZERparseSelectorAct
ENDparseSelectorAct


BEGINmodExit
    CODESTARTmodExit;
ENDmodExit


BEGINqueryEtryPt
    CODESTARTqueryEtryPt;
    CODEqueryEtryPt_STD_OMOD_QUERIES;
    CODEqueryEtryPt_STD_OMOD8_QUERIES;
    CODEqueryEtryPt_STD_CONF2_QUERIES;
    CODEqueryEtryPt_STD_CONF2_OMOD_QUERIES;
ENDqueryEtryPt


BEGINmodInitNoPredecl(UsrMsg)
    CODESTARTmodInit;
    INITLegCnfVars;
    *ipIFVersProvided = CURR_MOD_IF_VERSION; /* we only support the current interface specification */
    CODEmodInit_QueryRegCFSLineHdlr;
ENDmodInit

/* vim:set ai:
 */
