/* This file is part of GNU Pies.
   Copyright (C) 2007-2025 Sergey Poznyakoff

   GNU Pies is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   GNU Pies is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with GNU Pies.  If not, see <http://www.gnu.org/licenses/>. */
#include <config.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <syslog.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include <time.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <netdb.h>
#include "pies.h"
#include "pies_syslog.h"

struct log_message
{
  char *text;      /* Message text (no terminating nul). */
  size_t len;      /* Number of bytes in text */
  size_t drop_count; /* Count of messages dropped so far (first message only) */
  int trunc;
  struct log_message *next;
};

struct log_message_in
{
  struct log_message msg;
  char buf[PIES_LOG_BUF_SIZE];
};

/* Global variables */
/* Fallback log file is used to log critical messages when syslog
   daemon is unavailable.  If NULL, stderr will be used. */
char *pies_fallback_file = FALLBACK_LOG;

/* Log tag */
char *pies_log_tag = "pies";
/* Log facility */
int pies_log_facility = LOG_USER;
/* Maximum capacity of the log message queue */
size_t pies_log_max_queue = PIES_LOG_MAX_QUEUE;

/* Name of the syslog device.  If starts with a slash, it is assumed
   to be a UNIX socket name.  Otherwise, it is assumed to be a host name
   or IPv4 address of the syslog daemon, optionally followed by a colon
   and port number or service name.
   If NULL, PIES_LOG_DEV is assumed.
   The user can modify this value using the pies_syslog_set_dev call.
*/
static char *log_dev;

enum
  {
    LOGFD_NOT_OPEN = -1,
    LOGFD_DISABLED = -2
  };
/* Log socket descriptor. */
static int log_fd = LOGFD_NOT_OPEN;

/* Socket address */
static union {
    struct sockaddr_in s_in;
    struct sockaddr_un s_un;
} log_sa;

/* Socket address length. */
static socklen_t log_salen = 0;

/* Socked address family. */
static int log_family;

static inline int
pri_facility (int pri)
{
  return pri & ~0x7;
}

static inline int
pri_severity (int pri)
{
  return pri & 0x7;
}

/* Fallback logger */
static void
fallback_log (char const *fmt, ...)
{
  FILE *fp = NULL;
  va_list ap;

  if (pies_fallback_file)
    fp = fopen (pies_fallback_file, "a");
  if (!fp)
    fp = stderr;
  fprintf (fp, "pies[%lu]: ", (unsigned long) getpid());
  va_start (ap, fmt);
  vfprintf (fp, fmt, ap);
  va_end (ap);
  fputc ('\n', fp);
  if (fp != stderr)
    fclose (fp);
}

static int
reopen_logger (void)
{
  int fd;
  int flags;
  char *dev = log_dev ? log_dev : PIES_LOG_DEV;
  
  if (log_salen == 0)
    {
      if (dev[0] == '/')
	{
	  size_t len = strlen (dev);
	  if (len >= sizeof log_sa.s_un.sun_path)
	    {
	      fallback_log ("%s: UNIX socket name too long", dev);
	      log_fd = LOGFD_DISABLED;
	      return -1;
	    }
	  strcpy (log_sa.s_un.sun_path, dev);
	  log_sa.s_un.sun_family = AF_UNIX;
	  log_family = PF_UNIX;
	  log_salen = sizeof (log_sa.s_un);
	}
      else
	{
	  struct addrinfo hints;
	  struct addrinfo *res;
	  int rc;
	  char *node;
	  char *service;
	  
	  node = strdup (dev);
	  if (!node)
	    return -1;
	  
	  service = strchr (node, ':');
	  if (service)
	    *service++ = 0;
	  else
	    service = "syslog";
	  
	  memset (&hints, 0, sizeof (hints));
	  hints.ai_family = AF_INET;
	  hints.ai_socktype = SOCK_DGRAM;
	  hints.ai_protocol = IPPROTO_UDP;
	  
	  rc = getaddrinfo (node, service, &hints, &res);
	  free(node);
	  if (rc)
	    {
	      fallback_log ("%s: invalid socket address", dev);
	      log_fd = LOGFD_DISABLED;
	      return -1;
	    }
	  
	  memcpy (&log_sa, res->ai_addr, res->ai_addrlen);
	  log_family = PF_INET;
	  log_salen = res->ai_addrlen;
	  freeaddrinfo (res);
	}
    }

  fd = socket (log_family, SOCK_DGRAM, 0);

  if (fd == -1)
    {
      fallback_log ("can't create socket (family %d): %s",
		    log_family, strerror (errno));
      switch (errno)
	{
	case EAGAIN:
	case EINTR:
	case EMFILE:
	case ENFILE:
	  break;

	default:
	  log_fd = LOGFD_DISABLED;
	}
      return -1;
    }

  if ((flags = fcntl (fd, F_GETFL)) == -1 ||
      fcntl (fd, F_SETFL, flags | O_NONBLOCK) == -1 ||
      (flags = fcntl (fd, F_GETFD)) == -1 ||
      fcntl (fd, F_SETFD, flags | FD_CLOEXEC) == -1)
    {
      close (fd);
      log_fd = LOGFD_DISABLED;
      return -1;
    }
    
  if (connect (fd, (struct sockaddr*)&log_sa, log_salen))
    {
      fallback_log ("can't connect to log socket %s: %s", dev,
		    strerror (errno));
      close (fd);
      switch (errno) {
      case EAGAIN:
      case EINTR:
      case ECONNREFUSED:
	break;
	
      default:
	log_fd = LOGFD_DISABLED;
      }
      return -1;
    }

  log_fd = fd;
  
  return 0;
}

static struct log_message_in *
log_message_in_create (void)
{
  struct log_message_in *msg;

  if ((msg = malloc (sizeof (*msg))) != NULL)
    {
      msg->msg.text = msg->buf;
      msg->msg.len = 0;
      msg->msg.drop_count = 0;
      msg->msg.trunc = 0;
      msg->msg.next = NULL;
    }
  return msg;
}

static void
log_message_in_format (struct log_message_in *msg,
		       int prio, char const *msgtext, char const *tag,
		       pid_t pid)
{
  char tbuf[sizeof ("Jan  1 00:00:00")];
  char hostbuf[256];
  struct timeval tv;
  struct tm tm;
  int n;
  
  gettimeofday (&tv, NULL);
  localtime_r (&tv.tv_sec, &tm);
  strftime (tbuf, sizeof (tbuf), "%b %d %H:%M:%S", &tm);

  /* Supply default facility, unless prio already contains one.
     Note: this means that we cannot use LOG_KERN, but that doesn't
     really matter as we're not a kernel, anyway. */
  if (pri_facility (prio) == 0)
    prio |= pies_log_facility;
  
  if (log_family == PF_UNIX)
    {
      n = snprintf (msg->buf, sizeof (msg->buf),
		    "<%d>%s %s[%lu]: %s", 
		    prio,
		    tbuf,
		    tag,
		    (unsigned long)pid,
		    msgtext);
    }
  else
    {
      gethostname (hostbuf, sizeof (hostbuf));
      n = snprintf (msg->buf, sizeof (msg->buf),
		    "<%d>%s %s %s[%lu]: %s", 
		    prio, 
		    tbuf,
		    hostbuf,
		    tag,
		    (unsigned long)pid,
		    msgtext);
    }
  if (n >= sizeof (msg->buf))
    {
      /* Overflow */
      n = sizeof (msg->buf);
      msg->msg.trunc = 1;
    }
  msg->msg.len = n;
}

static struct log_message *
log_message_create (int prio, char const *msgtext, char const *tag, pid_t pid)
{
  struct log_message_in *msg;

  if ((msg = log_message_in_create ()) != NULL)
    log_message_in_format (msg, prio, msgtext, tag, pid);
  return &msg->msg;
}

/* Log message queue */
static struct log_message *log_queue_head, *log_queue_tail;
static size_t log_queue_length;

static void
log_message_putback (struct log_message *msg)
{
  msg->next = log_queue_head;
  log_queue_head = msg;
  if (!log_queue_tail)
    log_queue_tail = msg;
  log_queue_length++;
}
  
static struct log_message *
log_message_dequeue (void)
{
  struct log_message *msg = log_queue_head;
  if (msg)
    {
      log_queue_head = msg->next;
      if (!log_queue_head)
	log_queue_tail = log_queue_head;
      msg->next = NULL;
      log_queue_length--;
    }
  return msg;
}

static void
log_message_enqueue (struct log_message *inmsg)
{
  int truncated = 0;
  
  do
    {
      if (log_queue_length == pies_log_max_queue)
	{
	  struct log_message *msg;
	  struct log_message_in *tmp;
	  char buf[PIES_LOG_BUF_SIZE];
      
	  /* Dequeue first message */
	  msg = log_message_dequeue ();
      
	  if (msg->drop_count == 0)
	    {
	      /* If it is not a drop message, free it and create a new
		 drop message */
	      free (msg);
	      tmp = log_message_in_create ();
	      tmp->msg.drop_count = 1;
	    }
	  else
	    /* Otherwise, cast it to log_message_in */
	    tmp = (struct log_message_in *)msg;

	  /* Dequeue and drop the first message */
	  free (log_message_dequeue ());
	  tmp->msg.drop_count++;
	  
	  /* Reformat the message text */
	  snprintf (buf, sizeof (buf), "%zu messages dropped",
		    tmp->msg.drop_count);
	  log_message_in_format (tmp,
				 pies_log_facility|LOG_CRIT,
				 buf,
				 pies_log_tag,
				 getpid ());
	  
	  log_message_putback (&tmp->msg);
	}

      if (log_queue_tail)
	log_queue_tail->next = inmsg;
      else
	log_queue_head = inmsg;
      log_queue_tail = inmsg;  
      log_queue_length++;

      if (truncated)
	/* Make sure at most one truncation message is added */
	truncated = 0;
      else if (inmsg->trunc)
	{
	  inmsg = log_message_create (pies_log_facility|LOG_CRIT,
				      "-- message truncated --",
				      pies_log_tag, getpid ());
	  truncated = 1;
	}
    }
  while (truncated);
  
  pies_syslog_flush ();
}

/*
 * Flush the message queue to the socket.
 * Some fragments borrowed from the excellent syslog_async written by
 * Simon Kelley (http://www.thekelleys.org.uk/syslog-async).
 */
void
pies_syslog_flush (void)
{
  struct log_message *msg;
  int rc;
  
  while ((msg = log_message_dequeue ()) != NULL)
    {
      switch (log_fd)
	{
	case LOGFD_NOT_OPEN:
	  reopen_logger ();
	  break;

	case LOGFD_DISABLED:
	  fallback_log ("%*.*s", msg->len, msg->len, msg->text);
	  continue;
	}
      
      rc = send (log_fd, msg->text, msg->len, MSG_NOSIGNAL);
      if (rc != -1)
	{
	  free (msg);
	  continue;
	}
      log_message_putback (msg);

      if (errno == EINTR)
	continue;//Should not happen??
      if (errno == EAGAIN)
	break;
	    
      /* *BSD, returns this instead of blocking? */
      if (errno == ENOBUFS)
	break;

      /* A stream socket closed at the other end goes into EPIPE
	 forever, close and re-open. */
      if (errno == EPIPE)
	{
	  close (log_fd);
	  log_fd = LOGFD_NOT_OPEN;
	  continue;
	}

      if (errno == ECONNREFUSED || /* connection went down */
	  errno == ENOTCONN ||     /* nobody listening */
	  errno == EDESTADDRREQ || /* BSD equivalents of the above */ 
	  errno == ECONNRESET)
	{
	  /* The reader is gone.  Try reconnecting.  If failed,
	     retry when called next time. */

	  if (connect (log_fd, (struct sockaddr *)&log_sa, log_salen) != -1)
	    /* Connected successfully: retry now */
	    continue;
	  
	  if (errno == ENOENT || 
	      errno == EALREADY || 
	      errno == ECONNREFUSED ||
	      errno == EISCONN || 
	      errno == EINTR ||
	      errno == EAGAIN)
	    /* try again when woken up again */
	    break;
	}
		
      /* Else ? */
    }
}

void
logger_log (int prio, char const *msgtext, char const *tag, pid_t pid)
{
  struct log_message *msg;
  
  msg = log_message_create (prio, msgtext, tag, pid);
  if (msg)
    log_message_enqueue (msg);
}

/* Upper level logger API */
void
pies_vsyslog (int pri, char const *fmt, va_list ap)
{
  char buf[PIES_LOG_BUF_SIZE];
  vsnprintf (buf, sizeof(buf), fmt, ap);
  logger_log (pri, buf, pies_log_tag, getpid ());
}

void
pies_syslog (int pri, char const *fmt, ...)
{
  va_list ap;
  va_start (ap, fmt);
  pies_vsyslog (pri, fmt, ap);
  va_end (ap);
}

int
pies_syslog_open (void)
{
  return reopen_logger ();
}

void
pies_syslog_close (void)
{
  if (log_fd >= 0)
    close (log_fd);
  log_fd = LOGFD_NOT_OPEN;
}

void
pies_syslog_message (int prio, char const *text, char const *tag, pid_t pid)
{
  log_message_enqueue (log_message_create (prio, text, tag, pid));
}

int
pies_syslog_set_dev (char const *dev)
{
  char *p = strdup (dev);
  if (!p)
    return -1;
  pies_syslog_close ();
  free (log_dev);
  log_dev = p;
  log_salen = 0;
  return 0;
}
