/* improg.c * This input plugin enables rsyslog to execute a program and * receive from it the message stream as standard input. * One message per line with a maximum size * * NOTE: read comments in module-template.h for more specifics! * * File begun on 2009-04-01 by RGerhards * File copied and adjust from improg.c on 2019-02-07 by Ph. Duveau * * Copyright 2009-2018 Adiscon GmbH. * * This file is contribution 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 #include #include #include #include #include #include #include #include #include #include #include #include #include /* Very strange error on solaris 11 LOG_CRON already defined here. * It is redefined in rsyslog.h * The error also appeared with module omprog but the warning is * accepted without generated an error */ #ifdef LOG_CRON #undef LOG_CRON #endif #include "rsyslog.h" #include "conf.h" #include "syslogd-types.h" #include "srUtils.h" #include "template.h" #include "module-template.h" #include "unicode-helper.h" #include "errmsg.h" #include "cfsysline.h" #include "glbl.h" #include "prop.h" #include "ruleset.h" #include "ratelimit.h" #include "stringbuf.h" MODULE_TYPE_INPUT MODULE_TYPE_NOKEEP MODULE_CNFNAME("improg") struct instanceConf_s { uchar *pszBinary; /* name of external program to call */ char **aParams; /* optional parameters to pass to external program */ int iParams; /* holds the count of parameters if set */ uchar *pszTag; size_t lenTag; int iFacility; int iSeverity; int bConfirmMessages; /* does the program provide feedback via stdout? */ int bSignalOnClose; /* should send SIGTERM to program before closing pipe? */ long lCloseTimeout; /* how long to wait for program to terminate after closing pipe (ms) */ int bKillUnresponsive; /* should send SIGKILL if closeTimeout is reached? */ cstr_t *ppCStr; int bIsRunning; /* is program currently running? 0-no, 1-yes */ pid_t pid; /* pid of currently running child process */ int fdPipeToChild; /* fd for sending data to the program */ int fdPipeFromChild; /* fd for receiving messages from the program, or -1 */ uchar *pszBindRuleset; ruleset_t *pBindRuleset; /* ruleset to bind listener to (use system default if unspecified) */ ratelimit_t *ratelimiter; struct instanceConf_s *next; struct instanceConf_s *prev; }; /* config variables */ struct modConfData_s { rsconf_t *pConf; /* our overall config object */ }; /* internal structures */ DEF_OMOD_STATIC_DATA DEFobjCurrIf(glbl) DEFobjCurrIf(prop) DEFobjCurrIf(ruleset) static prop_t *pInputName = NULL; #define NO_HUP_FORWARD -1 /* indicates that HUP should NOT be forwarded */ #define DEFAULT_CONFIRM_TIMEOUT_MS 10000 #define DEFAULT_CLOSE_TIMEOUT_MS 5000 #define RESPONSE_LINE_BUFFER_SIZE 4096 #define OUTPUT_CAPTURE_BUFFER_SIZE 4096 #define MAX_FD_TO_CLOSE 65535 static instanceConf_t *confRoot = NULL; static fd_set rfds; static int nfds = 0; extern char **environ; /* POSIX environment ptr, by std not in a header... (see man 7 environ) */ static inline void std_checkRuleset_genErrMsg(__attribute__((unused)) modConfData_t *modConf, instanceConf_t *pInst) { LogError(0, NO_ERRCODE, "improg: ruleset '%s' for binary %s not found - " "using default ruleset instead", pInst->pszBindRuleset, pInst->pszBinary); } #include "im-helper.h" /* must be included AFTER the type definitions! */ /* tables for interfacing with the v6 config system */ /* action (instance) parameters */ static struct cnfparamdescr inppdescr[] = { { "binary", eCmdHdlrString, CNFPARAM_REQUIRED }, { "tag", eCmdHdlrString, CNFPARAM_REQUIRED }, { "severity", eCmdHdlrSeverity, 0 }, { "facility", eCmdHdlrFacility, 0 }, { "ruleset", eCmdHdlrString, 0 }, { "confirmmessages", eCmdHdlrBinary, 0 }, { "signalonclose", eCmdHdlrBinary, 0 }, { "closetimeout", eCmdHdlrInt, 0 }, { "killunresponsive", eCmdHdlrBinary, 0 } }; static struct cnfparamblk inppblk = { CNFPARAMBLK_VERSION, sizeof(inppdescr)/sizeof(struct cnfparamdescr), inppdescr }; /* execute the external program (must be called in child context after fork). */ static __attribute__((noreturn)) void execBinary(const instanceConf_t *pInst, int pipeToParent, int pipeFromParent) { int maxFd, fd, sigNum; struct sigaction sigAct; sigset_t sigSet; char errStr[1024]; if(dup2(pipeToParent, STDOUT_FILENO) == -1) { goto failed; } if(pipeFromParent != -1) { if(dup2(pipeFromParent, STDIN_FILENO) == -1) { goto failed; } } /* close the file handles the child process doesn't need (all above STDERR). * The following way is simple and portable, though not perfect. * See https://stackoverflow.com/a/918469 for alternatives. */ maxFd = sysconf(_SC_OPEN_MAX); if(maxFd < 0 || maxFd > MAX_FD_TO_CLOSE) { maxFd = MAX_FD_TO_CLOSE; } # ifdef VALGRIND else { maxFd -= 10; } # endif for(fd = STDERR_FILENO + 1 ; fd <= maxFd ; ++fd) { close(fd); } /* reset signal handlers to default */ memset(&sigAct, 0, sizeof(sigAct)); sigemptyset(&sigAct.sa_mask); sigAct.sa_handler = SIG_DFL; for(sigNum = 1 ; sigNum < NSIG ; ++sigNum) { sigaction(sigNum, &sigAct, NULL); } /* we need to block SIGINT, otherwise our program is cancelled when we are * stopped in debug mode. */ sigAct.sa_handler = SIG_IGN; sigaction(SIGINT, &sigAct, NULL); sigemptyset(&sigSet); sigprocmask(SIG_SETMASK, &sigSet, NULL); alarm(0); /* finally exec program */ execve((char*)pInst->pszBinary, pInst->aParams, environ); failed: /* an error occurred: log it and exit the child process. We use the * 'syslog' system call to log the error (we cannot use LogMsg/LogError, * since these functions add directly to the rsyslog input queue). */ rs_strerror_r(errno, errStr, sizeof(errStr)); DBGPRINTF("improg: failed to execute program '%s': %s\n", pInst->pszBinary, errStr); openlog("rsyslogd", 0, LOG_SYSLOG); syslog(LOG_ERR, "improg: failed to execute program '%s': %s\n", pInst->pszBinary, errStr); /* let's print the error to stderr for test bench purposes */ fprintf(stderr, "improg: failed to execute program '%s': %s\n", pInst->pszBinary, errStr); exit(1); } /* creates a pipe and starts program, uses pipe as stdin for program. * rgerhards, 2009-04-01 */ static rsRetVal openPipe(instanceConf_t *pInst) { int pipeFromChild[2] = { -1, -1 }; int pipeToChild[2] = { -1, -1 }; pid_t cpid; DEFiRet; /* if the 'confirmMessages' setting is enabled, open a pipe to send message confirmations to the program */ if(pInst->bConfirmMessages && pipe(pipeToChild) == -1) { ABORT_FINALIZE(RS_RET_ERR_CREAT_PIPE); } /* open a pipe to receive messages to the program */ if(pipe(pipeFromChild) == -1) { ABORT_FINALIZE(RS_RET_ERR_CREAT_PIPE); } DBGPRINTF("improg: executing program '%s' with '%d' parameters\n", pInst->pszBinary, pInst->iParams); cpid = fork(); if(cpid == -1) { ABORT_FINALIZE(RS_RET_ERR_FORK); } if(cpid == 0) { /* we are now the child process: execute the program */ /* close the pipe ends that the child doesn't need */ close(pipeFromChild[0]); if(pipeToChild[1] != -1) { close(pipeToChild[1]); } execBinary(pInst, pipeFromChild[1], pipeToChild[0]); /* NO CODE HERE - WILL NEVER BE REACHED! */ } DBGPRINTF("improg: child has pid %ld\n", (long int) cpid); /* close the pipe ends that the parent doesn't need */ close(pipeFromChild[1]); if(pipeToChild[0] != -1) { close(pipeToChild[0]); } pInst->fdPipeToChild = pipeToChild[1]; /* we'll send messages confirmations to the program via this fd */ pInst->fdPipeFromChild = pipeFromChild[0]; /* we'll receive message via this fd */ FD_SET(pInst->fdPipeFromChild, &rfds); /* manage select read fd set */ nfds = (nfds > pInst->fdPipeFromChild) ? nfds : pInst->fdPipeFromChild+1; pInst->pid = cpid; pInst->bIsRunning = 1; finalize_it: if(iRet != RS_RET_OK) { if(pipeFromChild[0] != -1) { close(pipeFromChild[0]); close(pipeFromChild[1]); } if(pipeToChild[0] != -1) { close(pipeToChild[0]); close(pipeToChild[1]); } } RETiRet; } static void waitForChild(instanceConf_t *pInst) { int status; int ret; long counter; counter = pInst->lCloseTimeout / 10; while ((ret = waitpid(pInst->pid, &status, WNOHANG)) == 0 && counter > 0) { srSleep(0, 10000); /* 0 seconds, 10 milliseconds */ --counter; } if (ret == 0) { /* timeout reached */ if (!pInst->bKillUnresponsive) { LogMsg(0, NO_ERRCODE, LOG_WARNING, "improg: program '%s' (pid %ld) did not terminate " "within timeout (%ld ms); ignoring it", pInst->pszBinary, (long int)pInst->pid, pInst->lCloseTimeout); return; } LogMsg(0, NO_ERRCODE, LOG_WARNING, "improg: program '%s' (pid %ld) did not terminate " "within timeout (%ld ms); killing it", pInst->pszBinary, (long int)pInst->pid, pInst->lCloseTimeout); if (kill(pInst->pid, SIGKILL) == -1) { LogError(errno, RS_RET_SYS_ERR, "improg: could not send SIGKILL to child process"); return; } ret = waitpid(pInst->pid, &status, 0); } /* waitpid will fail with errno == ECHILD if the child process has already been reaped by the rsyslogd main loop (see rsyslogd.c) */ if(ret == pInst->pid) { glblReportChildProcessExit(runConf, pInst->pszBinary, pInst->pid, status); } } /* Send SIGTERM to child process if configured to do so, close pipe * and wait for child to terminate. */ static void terminateChild(instanceConf_t *pInst) { if(pInst->bIsRunning) { if(pInst->fdPipeFromChild != -1) { close(pInst->fdPipeFromChild); FD_CLR(pInst->fdPipeFromChild, &rfds); pInst->fdPipeFromChild = -1; } if(pInst->fdPipeToChild != -1) { close(pInst->fdPipeToChild); pInst->fdPipeToChild = -1; } /* wait for the child AFTER closing the pipe, so it receives EOF */ waitForChild(pInst); pInst->bIsRunning = 0; } } static rsRetVal startChild(instanceConf_t *pInst) { DEFiRet; if (!pInst->bIsRunning) CHKiRet(openPipe(pInst)); finalize_it: if(iRet != RS_RET_OK && pInst->bIsRunning) { /* if initialization has failed, terminate program */ terminateChild(pInst); } RETiRet; } static rsRetVal enqLine(instanceConf_t *const __restrict__ pInst) { DEFiRet; smsg_t *pMsg; if(cstrLen(pInst->ppCStr) == 0) { /* we do not process empty lines */ FINALIZE; } CHKiRet(msgConstruct(&pMsg)); MsgSetMSGoffs(pMsg, 0); MsgSetHOSTNAME(pMsg, glbl.GetLocalHostName(), ustrlen(glbl.GetLocalHostName())); MsgSetFlowControlType(pMsg, eFLOWCTL_FULL_DELAY); MsgSetInputName(pMsg, pInputName); MsgSetAPPNAME(pMsg, (const char*)pInst->pszTag); MsgSetTAG(pMsg, pInst->pszTag, pInst->lenTag); msgSetPRI(pMsg, pInst->iFacility | pInst->iSeverity); MsgSetRawMsg(pMsg, (const char*)rsCStrGetBufBeg(pInst->ppCStr), cstrLen(pInst->ppCStr)); MsgSetRuleset(pMsg, pInst->pBindRuleset); ratelimitAddMsg(pInst->ratelimiter, NULL, pMsg); finalize_it: RETiRet; } /* read line(s) from the external program and sent them when they are complete */ static rsRetVal readChild(instanceConf_t *const pInst){ char c; int retval; while ((retval = read(pInst->fdPipeFromChild, &c, 1)) == 1) { if (c=='\n'){ enqLine(pInst); /* if confirm required then send an ACK to the program */ if (pInst->bConfirmMessages) { if (write(pInst->fdPipeToChild,"ACK\n",sizeof("ACK\n")-1) <= 0) LogMsg(0, NO_ERRCODE, LOG_WARNING, "improg: pipe to child seems to be closed."); } rsCStrTruncate(pInst->ppCStr, rsCStrLen(pInst->ppCStr)); } else { cstrAppendChar(pInst->ppCStr, c); } } return (retval == 0) ? RS_RET_OK : RS_RET_IO_ERROR; } /* create input instance, set default parameters, and * add it to the list of instances. */ static rsRetVal ATTR_NONNULL(1) createInstance(instanceConf_t **const ppInst) { instanceConf_t *pInst; DEFiRet; CHKmalloc(pInst = malloc(sizeof(instanceConf_t))); pInst->next = NULL; pInst->pszBindRuleset = NULL; pInst->pBindRuleset = NULL; pInst->ratelimiter = NULL; pInst->iSeverity = 5; pInst->iFacility = 128; pInst->pszTag = NULL; pInst->lenTag = 0; pInst->bIsRunning = 0; pInst->pid = -1; pInst->fdPipeToChild = -1; pInst->fdPipeFromChild = -1; pInst->pszBinary = NULL; pInst->aParams = NULL; pInst->iParams = 0; pInst->bConfirmMessages = 1; pInst->bSignalOnClose = 0; pInst->lCloseTimeout = 200; pInst->bKillUnresponsive = 1; *ppInst = pInst; finalize_it: RETiRet; } /* This adds a new listener object to the bottom of the list, but * it does NOT initialize any data members except for the list * pointers themselves. */ static rsRetVal ATTR_NONNULL() lstnAdd(instanceConf_t *pInst) { DEFiRet; CHKiRet(ratelimitNew(&pInst->ratelimiter, "improg", (char*)pInst->pszBinary)); /* insert it at the begin of the list */ pInst->prev = NULL; pInst->next = confRoot; if (confRoot != NULL) confRoot->prev = pInst; confRoot = pInst; finalize_it: RETiRet; } /* delete a listener object */ static void ATTR_NONNULL(1) lstnFree(instanceConf_t *pInst) { DBGPRINTF("lstnFree called for %s\n", pInst->pszBinary); if (pInst->ratelimiter != NULL) ratelimitDestruct(pInst->ratelimiter); if(pInst->pszBinary != NULL) free(pInst->pszBinary); if (pInst->pszTag) free(pInst->pszTag); if (pInst->pszBindRuleset != NULL) free(pInst->pszBindRuleset); if (pInst->aParams) { int i; for (i = 0;pInst->aParams[i]; i++) free(pInst->aParams[i]); free(pInst->aParams); } if (pInst->ppCStr) rsCStrDestruct(&pInst->ppCStr); free(pInst); } /* read */ BEGINnewInpInst struct cnfparamvals *pvals; instanceConf_t *pInst = NULL; int i; CODESTARTnewInpInst DBGPRINTF("newInpInst (improg)\n"); pvals = nvlstGetParams(lst, &inppblk, NULL); if(pvals == NULL) { ABORT_FINALIZE(RS_RET_MISSING_CNFPARAMS); } if(Debug) { DBGPRINTF("input param blk in improg:\n"); cnfparamsPrint(&inppblk, pvals); } CHKiRet(createInstance(&pInst)); for(i = 0 ; i < inppblk.nParams ; ++i) { if(!pvals[i].bUsed) continue; if(!strcmp(inppblk.descr[i].name, "binary")) { CHKiRet(split_binary_parameters(&pInst->pszBinary, &pInst->aParams, &pInst->iParams, pvals[i].val.d.estr)); } else if(!strcmp(inppblk.descr[i].name, "tag")) { pInst->pszTag = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL); pInst->lenTag = es_strlen(pvals[i].val.d.estr); } else if(!strcmp(inppblk.descr[i].name, "ruleset")) { pInst->pszBindRuleset = (uchar*)es_str2cstr(pvals[i].val.d.estr, NULL); } else if(!strcmp(inppblk.descr[i].name, "severity")) { pInst->iSeverity = pvals[i].val.d.n; } else if(!strcmp(inppblk.descr[i].name, "facility")) { pInst->iFacility = pvals[i].val.d.n; } else if(!strcmp(inppblk.descr[i].name, "confirmmessages")) { pInst->bConfirmMessages = pvals[i].val.d.n; } else if(!strcmp(inppblk.descr[i].name, "signalonclose")) { pInst->bSignalOnClose = pvals[i].val.d.n; } else if(!strcmp(inppblk.descr[i].name, "closetimeout")) { pInst->lCloseTimeout = pvals[i].val.d.n; } else if(!strcmp(inppblk.descr[i].name, "killunresponsive")) { pInst->bKillUnresponsive = pvals[i].val.d.n; } else { DBGPRINTF("program error, non-handled " "param '%s'\n", inppblk.descr[i].name); } } if(pInst->pszBinary == NULL) { LogError(0, RS_RET_FILE_NOT_SPECIFIED, "ulogbase is not configured - no input will be gathered"); ABORT_FINALIZE(RS_RET_FILE_NOT_SPECIFIED); } CHKiRet(cstrConstruct(&pInst->ppCStr)); if ((iRet = lstnAdd(pInst)) != RS_RET_OK) { ABORT_FINALIZE(iRet); } finalize_it: CODE_STD_FINALIZERnewInpInst if (pInst && iRet != RS_RET_OK) lstnFree(pInst); cnfparamvalsDestruct(pvals, &inppblk); ENDnewInpInst BEGINwillRun CODESTARTwillRun /* we need to create the inputName property (only once during our lifetime) */ CHKiRet(prop.Construct(&pInputName)); CHKiRet(prop.SetString(pInputName, UCHAR_CONSTANT("improg"), sizeof("improg") - 1)); CHKiRet(prop.ConstructFinalize(pInputName)); finalize_it: ENDwillRun BEGINrunInput struct timeval tv; int retval; instanceConf_t *pInst; CODESTARTrunInput FD_ZERO(&rfds); for(pInst = confRoot ; pInst != NULL ; pInst = pInst->next) { startChild(pInst); } for(pInst = confRoot ; pInst != NULL ; pInst = pInst->next) { if (pInst->bIsRunning && pInst->fdPipeToChild > 0){ if (write(pInst->fdPipeToChild, "START\n", sizeof("START\n")-1) <= 0) LogMsg(0, NO_ERRCODE, LOG_WARNING, "improg: pipe to child seems to be closed."); DBGPRINTF("Sending START to %s\n", pInst->pszBinary); } } /* main module loop */ tv.tv_usec = 1000; while (glbl.GetGlobalInputTermState() == 0) { fd_set temp; memcpy(&temp, &rfds, sizeof(fd_set)); tv.tv_sec = 0; /* wait for external data or 0.1 second */ retval = select(nfds, &temp, NULL, NULL, &tv); /* retval is the number of fd with data to read */ while (retval>0) { for (pInst = confRoot; pInst != NULL; pInst = pInst->next) { if (FD_ISSET(pInst->fdPipeFromChild, &temp)) { DBGPRINTF("read child %s\n",pInst->pszBinary); readChild(pInst); retval--; } } } tv.tv_usec = 100000; } DBGPRINTF("terminating upon request of rsyslog core\n"); ENDrunInput /* This function is called by the framework after runInput() has been terminated. It * shall free any resources and prepare the module for unload. * CODEqueryEtryPt_STD_IMOD_QUERIES */ BEGINafterRun CODESTARTafterRun instanceConf_t *pInst = confRoot, *nextInst; confRoot = NULL; DBGPRINTF("afterRun\n"); while(pInst != NULL) { nextInst = pInst->next; if (pInst->bIsRunning) { if (pInst->bSignalOnClose) { kill(pInst->pid, SIGTERM); LogMsg(0, NO_ERRCODE, LOG_INFO, "%s SIGTERM signaled.", pInst->aParams[0]); } if (pInst->fdPipeToChild > 0){ if (write(pInst->fdPipeToChild, "STOP\n", strlen("STOP\n")) <= 0 && !pInst->bSignalOnClose) LogMsg(0, NO_ERRCODE, LOG_WARNING, "improg: pipe to child seems to be closed."); } terminateChild(pInst); } lstnFree(pInst); pInst = nextInst; } if(pInputName != NULL) prop.Destruct(&pInputName); ENDafterRun BEGINisCompatibleWithFeature CODESTARTisCompatibleWithFeature if(eFeat == sFEATURERepeatedMsgReduction) { iRet = RS_RET_OK; } ENDisCompatibleWithFeature BEGINbeginCnfLoad CODESTARTbeginCnfLoad pModConf->pConf = pConf; ENDbeginCnfLoad BEGINendCnfLoad CODESTARTendCnfLoad ENDendCnfLoad BEGINcheckCnf instanceConf_t *pInst; CODESTARTcheckCnf for(pInst = confRoot ; pInst != NULL ; pInst = pInst->next) { std_checkRuleset(pModConf , pInst); } ENDcheckCnf BEGINactivateCnf CODESTARTactivateCnf ENDactivateCnf BEGINfreeCnf CODESTARTfreeCnf ENDfreeCnf BEGINmodExit CODESTARTmodExit objRelease(ruleset, CORE_COMPONENT); objRelease(glbl, CORE_COMPONENT); objRelease(prop, CORE_COMPONENT); ENDmodExit BEGINqueryEtryPt CODESTARTqueryEtryPt CODEqueryEtryPt_STD_IMOD_QUERIES CODEqueryEtryPt_STD_CONF2_QUERIES CODEqueryEtryPt_STD_CONF2_IMOD_QUERIES CODEqueryEtryPt_IsCompatibleWithFeature_IF_OMOD_QUERIES ENDqueryEtryPt BEGINmodInit() CODESTARTmodInit *ipIFVersProvided = CURR_MOD_IF_VERSION; /* we only support the current interface specification */ CODEmodInit_QueryRegCFSLineHdlr CHKiRet(objUse(ruleset, CORE_COMPONENT)); CHKiRet(objUse(glbl, CORE_COMPONENT)); CHKiRet(objUse(prop, CORE_COMPONENT)); ENDmodInit