/* combined imklog driver for BSD and Linux
 *
 * This contains OS-specific functionality to read the BSD
 * or Linux kernel log. For a general overview, see head comment in
 * imklog.c. This started out as the BSD-specific drivers, but it
 * turned out that on modern Linux the implementation details
 * are very small, and so we use a single driver for both OS's with
 * a little help of conditional compilation.
 *
 * Copyright 2008-2023 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.
 */
#ifdef HAVE_CONFIG_H
    #include "config.h"
#endif
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#ifdef OS_LINUX
    #include <sys/klog.h>
#endif

#include "rsyslog.h"
#include "srUtils.h"
#include "debug.h"
#include "imklog.h"

/* globals */
static int fklog = -1; /* kernel log fd */

#ifndef _PATH_KLOG
    #ifdef OS_LINUX
        #define _PATH_KLOG "/proc/kmsg"
    #else
        #define _PATH_KLOG "/dev/klog"
    #endif
#endif


#ifdef OS_LINUX
/* submit a message to imklog Syslog() API. In this function, we check if
 * a kernel timestamp is present and, if so, extract and strip it.
 * Note that this is heavily Linux specific and thus is not compiled or
 * used for BSD.
 * Special thanks to Lennart Poettering for suggesting on how to convert
 * the kernel timestamp to a realtime timestamp. This method depends on
 * the fact the the kernel timestamp is written using the monotonic clock.
 * Shall that change (very unlikely), this code must be changed as well. Note
 * that due to the way we generate the delta, we are unable to write the
 * absolutely correct timestamp (system call overhead of the clock calls
 * prevents us from doing so). However, the difference is very minor.
 * rgerhards, 2011-06-24
 */
static void submitSyslog(modConfData_t *pModConf, syslog_pri_t pri, uchar *buf) {
    long secs;
    long usecs;
    long secOffs;
    long usecOffs;
    unsigned i;
    unsigned bufsize;
    struct timespec monotonic, realtime;
    struct timeval tv;
    struct timeval *tp = NULL;

    /* find end of pri */
    int endpri = 1;
    while (buf[endpri] != '>' && endpri < 5) ++endpri;
    if (endpri > 4 || buf[endpri + 1] != '[') goto done;
    DBGPRINTF("imklog: kernel timestamp detected, extracting it\n");

    /* we now try to parse the timestamp. iff it parses, we assume
     * it is a timestamp. Otherwise we know for sure it is no ts ;)
     */
    i = endpri + 2; /* space or first digit after '[' */
    while (buf[i] && isspace(buf[i])) ++i; /* skip space */
    secs = 0;
    while (buf[i] && isdigit(buf[i])) {
        secs = secs * 10 + buf[i] - '0';
        ++i;
    }
    if (buf[i] != '.') {
        DBGPRINTF("no dot --> no kernel timestamp\n");
        goto done; /* no TS! */
    }

    ++i; /* skip dot */
    usecs = 0;
    while (buf[i] && isdigit(buf[i])) {
        usecs = usecs * 10 + buf[i] - '0';
        ++i;
    }
    if (buf[i] != ']') {
        DBGPRINTF("no trailing ']' --> no kernel timestamp\n");
        goto done; /* no TS! */
    }
    ++i; /* skip ']' */

    /* we have a timestamp */
    DBGPRINTF("kernel timestamp is %ld %ld\n", secs, usecs);
    if (!pModConf->bKeepKernelStamp) {
        bufsize = strlen((char *)buf);
        memmove(buf + endpri + 1, buf + i, bufsize - i + 1);
    }

    if (!pModConf->bParseKernelStamp) {
        DBGPRINTF("imklog/bsd: parseKernelStamp not set, ignoring kernel timestamp\n");
        goto done;
    }

    clock_gettime(CLOCK_MONOTONIC, &monotonic);
    clock_gettime(CLOCK_REALTIME, &realtime);
    secOffs = realtime.tv_sec - monotonic.tv_sec;
    usecOffs = (realtime.tv_nsec - monotonic.tv_nsec) / 1000;
    if (usecOffs < 0) {
        secOffs--;
        usecOffs += 1000000l;
    }

    usecs += usecOffs;
    if (usecs > 999999l) {
        secs++;
        usecs -= 1000000l;
    }
    secs += secOffs;
    tv.tv_sec = secs;
    tv.tv_usec = usecs;
    tp = &tv;

done:
    Syslog(pModConf, pri, buf, tp);
}
#else /* now comes the BSD "code" (just a shim) */
static void submitSyslog(modConfData_t *pModConf, syslog_pri_t pri, uchar *buf) {
    Syslog(pModConf, pri, buf, NULL);
}
#endif /* #ifdef LINUX */


static uchar *GetPath(modConfData_t *pModConf) {
    return pModConf->pszPath ? pModConf->pszPath : (uchar *)_PATH_KLOG;
}

/* open the kernel log - will be called inside the willRun() imklog
 * entry point. -- rgerhards, 2008-04-09
 */
rsRetVal klogWillRunPrePrivDrop(modConfData_t *pModConf) {
    char errmsg[2048];
    DEFiRet;

    fklog = open((char *)GetPath(pModConf), O_RDONLY, 0);
    if (fklog < 0) {
        imklogLogIntMsg(LOG_ERR, "imklog: cannot open kernel log (%s): %s.", GetPath(pModConf),
                        rs_strerror_r(errno, errmsg, sizeof(errmsg)));
        ABORT_FINALIZE(RS_RET_ERR_OPEN_KLOG);
    }

#ifdef OS_LINUX
    /* Set level of kernel console messaging.. */
    if (pModConf->console_log_level != -1) {
        int r = klogctl(8, NULL, pModConf->console_log_level);
        if (r != 0) {
            imklogLogIntMsg(LOG_WARNING, "imklog: cannot set console log level: %s",
                            rs_strerror_r(errno, errmsg, sizeof(errmsg)));
            /* make sure we do not try to re-set! */
            pModConf->console_log_level = -1;
        }
    }
#endif /* #ifdef OS_LINUX */

finalize_it:
    RETiRet;
}

/* make sure the kernel log is readable after dropping privileges
 */
rsRetVal klogWillRunPostPrivDrop(modConfData_t *pModConf) {
    char errmsg[2048];
    int r;
    DEFiRet;

    /* this normally returns EINVAL */
    /* on an OpenVZ VM, we get EPERM */
    r = read(fklog, NULL, 0);
    if (r < 0 && errno != EINVAL) {
        imklogLogIntMsg(LOG_ERR, "imklog: cannot open kernel log (%s): %s.", GetPath(pModConf),
                        rs_strerror_r(errno, errmsg, sizeof(errmsg)));
        fklog = -1;
        ABORT_FINALIZE(RS_RET_ERR_OPEN_KLOG);
    }

finalize_it:
    RETiRet;
}


/* Read kernel log while data are available, split into lines.
 */
static void readklog(modConfData_t *pModConf) {
    char *p, *q;
    int len, i;
    int iMaxLine;
    uchar bufRcv[128 * 1024 + 1];
    char errmsg[2048];
    uchar *pRcv = NULL; /* receive buffer */

    iMaxLine = klog_getMaxLine();

    /* we optimize performance: if iMaxLine is below our fixed size buffer (which
     * usually is sufficiently large), we use this buffer. if it is higher, heap memory
     * is used. We could use alloca() to achive a similar aspect, but there are so
     * many issues with alloca() that I do not want to take that route.
     * rgerhards, 2008-09-02
     */
    if ((size_t)iMaxLine < sizeof(bufRcv) - 1) {
        pRcv = bufRcv;
    } else {
        if ((pRcv = (uchar *)malloc(iMaxLine + 1)) == NULL) {
            iMaxLine = sizeof(bufRcv) - 1; /* better this than noting */
            pRcv = bufRcv;
        }
    }

    len = 0;
    for (;;) {
        dbgprintf("imklog(BSD/Linux) waiting for kernel log line\n");
        i = read(fklog, pRcv + len, iMaxLine - len);
        if (i > 0) {
            pRcv[i + len] = '\0';
        } else {
            if (i < 0 && errno != EINTR && errno != EAGAIN) {
                imklogLogIntMsg(LOG_ERR, "imklog: error reading kernel log - shutting down: %s",
                                rs_strerror_r(errno, errmsg, sizeof(errmsg)));
                fklog = -1;
            }
            break;
        }

        for (p = (char *)pRcv; (q = strchr(p, '\n')) != NULL; p = q + 1) {
            *q = '\0';
            submitSyslog(pModConf, LOG_INFO, (uchar *)p);
        }
        len = strlen(p);
        if (len >= iMaxLine - 1) {
            submitSyslog(pModConf, LOG_INFO, (uchar *)p);
            len = 0;
        }
        if (len > 0) memmove(pRcv, p, len + 1);
    }
    if (len > 0) submitSyslog(pModConf, LOG_INFO, pRcv);

    if (pRcv != bufRcv) free(pRcv);
}


/* to be called in the module's AfterRun entry point
 * rgerhards, 2008-04-09
 */
rsRetVal ATTR_NONNULL() klogAfterRun(modConfData_t *const pModConf __attribute__((unused))) {
    DEFiRet;
    if (fklog != -1) close(fklog);
#ifdef OS_LINUX
    /* Turn on logging of messages to console, but only if a log level was speficied */
    if (pModConf->console_log_level != -1) klogctl(7, NULL, 0);
#endif
    RETiRet;
}


/* to be called in the module's WillRun entry point, this is the main
 * "message pull" mechanism.
 * rgerhards, 2008-04-09
 */
rsRetVal klogLogKMsg(modConfData_t *pModConf) {
    DEFiRet;
    readklog(pModConf);
    RETiRet;
}


/* provide the (system-specific) default facility for internal messages
 * rgerhards, 2008-04-14
 */
int klogFacilIntMsg(void) {
    return LOG_SYSLOG;
}
