/*
**  L2 - OSSP Logging Library
**  Copyright (c) 2001 The OSSP Project (http://www.ossp.org/)
**  Copyright (c) 2001 Cable & Wireless Deutschland (http://www.cw.com/de/)
**
**  This file is part of OSSP L2, a flexible logging library which
**  can be found at http://www.ossp.org/pkg/l2/.
**
**  Permission to use, copy, modify, and distribute this software for
**  any purpose with or without fee is hereby granted, provided that
**  the above copyright notice and this permission notice appear in all
**  copies.
**
**  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
**  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
**  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
**  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
**  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
**  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
**  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
**  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
**  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
**  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
**  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
**  SUCH DAMAGE.
**
**  l2_ch_syslog.c: syslog(3) channel implementation
*/

#include "l2.h"
#include "l2_p.h"

#include <syslog.h>
#include <time.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <unistd.h>

/* declare private channel configuration */
typedef struct {
    char      *szTarget;
    char      *szRemoteHost;
    int        nRemotePort;
    char      *szLocalHost;
    char      *szFacility;
    int        nFacility;
    char      *szIdent;
    int        bLogPid;
    sa_t      *saRemoteSock;
    sa_addr_t *saaRemoteAddr;
} l2_ch_syslog_t;

/* mapping from L2 log levels to syslog(3) log levels */
static struct {
    int levelL2;
    int levelSL;
} l2_ch_syslog_L2toSL[] = {
    { L2_LEVEL_PANIC,    LOG_EMERG   },
    { L2_LEVEL_CRITICAL, LOG_CRIT    },
    { L2_LEVEL_ERROR,    LOG_ERR	 },
    { L2_LEVEL_WARNING,  LOG_WARNING },
    { L2_LEVEL_NOTICE,   LOG_NOTICE  },
    { L2_LEVEL_INFO,     LOG_INFO	 },
    { L2_LEVEL_TRACE,    LOG_INFO    },
    { L2_LEVEL_DEBUG,    LOG_DEBUG   },
    { -1,                -1          }
};

/* Syslog Facility Table */
static struct {
    char *name;      /* the common name */
    int   numREMOTE; /* number according to RFC3164 */
    int   numLOCAL;  /* local encoding */
} l2_ch_syslog_SLfac[] = {
    { "kern",      0,      LOG_KERN     },
    { "user",      1,      LOG_USER     },
    { "mail",      2,      LOG_MAIL     },
    { "daemon",    3,      LOG_DAEMON   },
    { "auth",      4,      LOG_AUTH     },
#ifndef LOG_SYSLOG
#define LOG_SYSLOG   (5 << 3)
#endif
    { "syslog",    5,      LOG_SYSLOG   },
    { "lpr",       6,      LOG_LPR      },
    { "news",      7,      LOG_NEWS     },
    { "uucp",      8,      LOG_UUCP     },
    { "cron",      9,      LOG_CRON     },
#ifndef LOG_AUTHPRIV
#define LOG_AUTHPRIV (10 << 3)
#endif
    { "authpriv", 10,      LOG_AUTHPRIV },
#ifndef LOG_FTP
#define LOG_FTP      (11 << 3)
#endif
    { "ftp",      11,      LOG_FTP      },
#ifndef LOG_NTP
#define LOG_NTP      (12 << 3)
#endif
    { "ntp",      12,      LOG_NTP      },
#ifndef LOG_SECURITY
#define LOG_SECURITY (13 << 3)
#endif
    { "security", 13,      LOG_SECURITY },
#ifndef LOG_CONSOLE
#define LOG_CONSOLE  (14 << 3)
#endif
    { "console",  14,      LOG_CONSOLE  },
#ifndef LOG_CLOCK
#define LOG_CLOCK    (15 << 3)
#endif
    { "clock",    15,      LOG_CLOCK    },
    { "local0",   16,      LOG_LOCAL0   },
    { "local1",   17,      LOG_LOCAL1   },
    { "local2",   18,      LOG_LOCAL2   },
    { "local3",   19,      LOG_LOCAL3   },
    { "local4",   20,      LOG_LOCAL4   },
    { "local5",   21,      LOG_LOCAL5   },
    { "local6",   22,      LOG_LOCAL6   },
    { "local7",   23,      LOG_LOCAL7   },
    { NULL,       0,       0            }
};

/* create channel */
static l2_result_t hook_create(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_syslog_t *cfg;
    struct utsname uts;
    char *cp;

    /* allocate private channel configuration */
    if ((cfg = (l2_ch_syslog_t *)malloc(sizeof(l2_ch_syslog_t))) == NULL)
        return L2_ERR_MEM;

    /* initialize configuration with reasonable defaults */
    cfg->szTarget      = strdup("local");
    cfg->szRemoteHost  = NULL;
    cfg->nRemotePort   = 514;
    if (uname(&uts) == 0) {
        cfg->szLocalHost = strdup(uts.nodename);
        if ((cp = strchr(cfg->szLocalHost, '.')) != NULL)
            *cp = '\0';
    }
    else
        cfg->szLocalHost = strdup("localhost");
    cfg->szFacility    = strdup("user");
    cfg->nFacility     = LOG_USER;
    cfg->szIdent       = NULL;
    cfg->bLogPid       = FALSE;
    cfg->saRemoteSock  = NULL;
    cfg->saaRemoteAddr = NULL;

    /* link private channel configuration into channel context */
    ctx->vp = cfg;

    return L2_OK;
}

/* configure channel */
static l2_result_t hook_configure(l2_context_t *ctx, l2_channel_t *ch, const char *fmt, va_list ap)
{
    l2_ch_syslog_t *cfg = (l2_ch_syslog_t *)ctx->vp;
    l2_param_t pa[8];
    l2_result_t rv;
    int i;

    /* feed and call generic parameter parsing engine */
    L2_PARAM_SET(pa[0], target,     STRING, &cfg->szTarget);
    L2_PARAM_SET(pa[1], remotehost, STRING, &cfg->szRemoteHost);
    L2_PARAM_SET(pa[2], remoteport, INT,    &cfg->nRemotePort);
    L2_PARAM_SET(pa[3], localhost,  STRING, &cfg->szLocalHost);
    L2_PARAM_SET(pa[4], facility,   STRING, &cfg->szFacility);
    L2_PARAM_SET(pa[5], ident,      STRING, &cfg->szIdent);
    L2_PARAM_SET(pa[6], logpid,     INT,    &cfg->bLogPid);
    L2_PARAM_END(pa[7]);

    /* sanity checking & post-processing */
    rv = l2_util_setparams(pa, fmt, ap);
    if (cfg->szTarget == NULL || cfg->szFacility == NULL)
        return L2_ERR_USE;
    if (!(   strcmp(cfg->szTarget, "local") == 0
          || strcmp(cfg->szTarget, "remote") == 0))
        return L2_ERR_USE;
    for (i = 0; l2_ch_syslog_SLfac[i].name != NULL; i++)
        if (strcmp(l2_ch_syslog_SLfac[i].name, cfg->szFacility) == 0)
            break;
    if (l2_ch_syslog_SLfac[i].name == NULL)
        return L2_ERR_USE;
    if (strcmp(cfg->szTarget, "local") == 0)
        cfg->nFacility = l2_ch_syslog_SLfac[i].numLOCAL;
    else
        cfg->nFacility = (l2_ch_syslog_SLfac[i].numREMOTE << 3);
    if (   strcmp(cfg->szTarget, "remote") == 0 
        && (cfg->szRemoteHost == NULL
            || (cfg->nRemotePort <= 0 || cfg->nRemotePort >= 65536)))
        return L2_ERR_USE;
    if (   cfg->szLocalHost == NULL
        || strchr(cfg->szLocalHost, '.') != NULL)
        return L2_ERR_USE;
    if (cfg->szIdent != NULL && strlen(cfg->szIdent) > (32-(1+5+1)))
        return L2_ERR_USE;
    return rv;
}

/* open channel */
static l2_result_t hook_open(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_syslog_t *cfg = (l2_ch_syslog_t *)ctx->vp;
    int opt;
    sa_rc_t rc;
    sa_addr_t *la;

    if (cfg->szIdent == NULL)
        return L2_ERR_USE;
    if (strcmp(cfg->szTarget, "local") == 0) {
        /* open local syslog connection via syslog(3) */
        opt = 0;
        if (cfg->bLogPid)
            opt |= LOG_PID;
        openlog(cfg->szIdent, opt, cfg->nFacility);
        /* setlogmask(0); */
    }
    else {
        /* open remote syslog connection via UDP socket */
        if (cfg->szRemoteHost == NULL)
            return L2_ERR_USE;
        if ((rc = sa_addr_create(&cfg->saaRemoteAddr)) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_INT);
        if ((rc = sa_addr_u2a(cfg->saaRemoteAddr, "inet://%s:%d", 
                              cfg->szRemoteHost, cfg->nRemotePort)) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_INT);
        if ((rc = sa_create(&cfg->saRemoteSock)) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_INT);
        sa_type(cfg->saRemoteSock, SA_TYPE_DATAGRAM);
        sa_timeout(cfg->saRemoteSock, SA_TIMEOUT_ALL, 10, 0);
        if ((rc = sa_addr_create(&la)) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_INT);
        /* FIXME: if uid == 0 -> use port 514 */
        if ((rc = sa_addr_u2a(la, "inet://0.0.0.0:0")) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_INT);
        if ((rc = sa_bind(cfg->saRemoteSock, la)) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_INT);
        sa_addr_destroy(la);
    }
    return L2_OK;
}

/* write to channel */
static l2_result_t hook_write(l2_context_t *ctx, l2_channel_t *ch,
                              l2_level_t level, const char *buf, size_t buf_size)
{
    l2_ch_syslog_t *cfg = (l2_ch_syslog_t *)ctx->vp;
    int prio;
    int i;
    char caTime[15+1];
    char caBuf[2048];
    time_t t;
    struct tm *tm;
    size_t n;
    sa_rc_t rc;

    /* determine syslog priority */
    prio = 0;
    for (i = 0; l2_ch_syslog_L2toSL[i].levelL2 != -1; i++) {
        if (l2_ch_syslog_L2toSL[i].levelL2 == level) {
            prio = l2_ch_syslog_L2toSL[i].levelSL;
            break;
        }
    }
    if (l2_ch_syslog_L2toSL[i].levelL2 == -1)
        return L2_ERR_USE;

    /* FIXME: nul-terminate buf? */

    if (strcmp(cfg->szTarget, "local") == 0) {
        /* send to local syslogd via syslog(3) */
        syslog(prio, buf);
    }
    else {
        /* send to remote syslogd via UDP */
        if (strlen(buf) > 1024)
            return L2_ERR_MEM;
        prio += cfg->nFacility;
        t = time(NULL);
        tm = localtime(&t);
        strftime(caTime, sizeof(caTime), "%b %d %H:%M:%S", tm);
        if (caTime[4] == '0')
            caTime[4] = ' ';
        if (cfg->bLogPid) 
            n = l2_util_sprintf(caBuf, sizeof(caBuf), "<%d>%s %s %s[%lu]: %s", 
                                prio, caTime, cfg->szLocalHost, 
                                cfg->szIdent, (unsigned long)getpid(), buf);
        else
            n = l2_util_sprintf(caBuf, sizeof(caBuf), "<%d>%s %s %s: %s", 
                                prio, caTime, cfg->szLocalHost, 
                                cfg->szIdent, buf);
        if ((n = strlen(caBuf)) > 1024)
            return L2_ERR_IO;
        if ((rc = sa_send(cfg->saRemoteSock, caBuf, n, 
                          NULL, cfg->saaRemoteAddr)) != SA_OK)
            return (rc == SA_ERR_SYS ? L2_ERR_SYS : L2_ERR_IO);
    }

    return L2_OK;
}

/* close channel */
static l2_result_t hook_close(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_syslog_t *cfg = (l2_ch_syslog_t *)ctx->vp;

    if (strcmp(cfg->szTarget, "local") == 0) {
        closelog();
    }
    else {
        if (cfg->saRemoteSock != NULL) {
            sa_destroy(cfg->saRemoteSock);
            cfg->saRemoteSock = NULL;
        }
        if (cfg->saaRemoteAddr != NULL) {
            sa_addr_destroy(cfg->saaRemoteAddr);
            cfg->saaRemoteAddr = NULL;
        }
    }
    return L2_OK;
}

/* destroy channel */
static l2_result_t hook_destroy(l2_context_t *ctx, l2_channel_t *ch)
{
    l2_ch_syslog_t *cfg = (l2_ch_syslog_t *)ctx->vp;

    /* destroy channel configuration */
    if (cfg->szTarget != NULL)
        free(cfg->szTarget);
    if (cfg->szRemoteHost != NULL)
        free(cfg->szRemoteHost);
    if (cfg->szLocalHost != NULL)
        free(cfg->szLocalHost);
    if (cfg->szFacility != NULL)
        free(cfg->szFacility);
    if (cfg->szIdent != NULL)
        free(cfg->szIdent);
    if (cfg->saRemoteSock != NULL)
        sa_destroy(cfg->saRemoteSock);
    if (cfg->saaRemoteAddr != NULL)
        sa_addr_destroy(cfg->saaRemoteAddr);
    free(cfg);

    return L2_OK;
}

/* exported channel handler structure */
l2_handler_t l2_handler_syslog = {
    L2_CHANNEL_OUTPUT,
    hook_create,
    hook_configure,
    hook_open,
    hook_write,
    NULL,
    hook_close,
    hook_destroy
};

