/* This file is part of GNU Pies.
   Copyright (C) 2007-2026 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;
int pies_fallback_ignore_failures;

/* 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;
}

struct fallback_logger
{
  int fd;
  char buf[BUFSIZ];
  int level;
};

static struct fallback_logger fblog = { .fd = LOGFD_NOT_OPEN };

static inline int
fallback_available (void)
{
  return BUFSIZ - fblog.level;
}

static void
fallback_close (void)
{
  if (fblog.fd >= 0)
    deregister_socket (fblog.fd);
  fblog.fd = LOGFD_NOT_OPEN;
}

static int
fallback_write (int fd, void *data)
{
  int n;

  n = write (fd, fblog.buf, fblog.level);
  if (n == -1)
    {
      switch (errno)
	{
	case EINTR:
	  break;

	default:
	  if (pies_fallback_ignore_failures)
	    {
	      fallback_close ();
	      fblog.fd = LOGFD_DISABLED;
	      fblog.level = 0;
	    }
	  else
	    abort ();
	}
    }
  else
    {
      if (n < fblog.level)
	memmove (fblog.buf, fblog.buf + n, fblog.level - n);
      fblog.level -= n;
      if (fblog.level == 0)
	disable_socket (fblog.fd);
    }
  return 0;
}

static int
fallback_open (void)
{
  if (pies_fallback_file)
    {
      int fd = open (pies_fallback_file, O_CREAT|O_WRONLY|O_APPEND, 0655);
      if (fd == -1)
	{
	  if (pies_fallback_ignore_failures)
	    {
	      fd = LOGFD_DISABLED;
	      fblog.level = 0;
	    }
	  else
	    abort ();
	}

      fblog.fd = fd;
    }
  else
    fblog.fd = 2;
  if (fblog.fd >= 0)
    register_socket (fblog.fd, NULL, fallback_write, NULL, NULL, NULL);
  return fblog.fd;
}

/* Fallback logger */
static void
fallback_log (char const *fmt, ...)
{
  va_list ap;
  char buf[BUFSIZ];
  int n;

  switch (fblog.fd)
    {
    case LOGFD_NOT_OPEN:
      if (fallback_open >= 0)
	break;
    case LOGFD_DISABLED:
      return;
    }

  enable_socket (fblog.fd);

  va_start (ap, fmt);
  n = vsnprintf (buf, sizeof (buf)-1, fmt, ap);
  va_end (ap);

  if (n == -1 || n >= sizeof (buf)-1 || !memchr (buf, '\0', n + 1))
    {
      static char ellipsis[] = "...";
      /* Output truncated */
      n = BUFSIZ-1;
      strcpy (buf + n - sizeof (ellipsis), ellipsis);
    }

  while (n + 1 > fallback_available ())
    {
      char *p;
      int n;

      if ((p = memchr (fblog.buf, '\n', fblog.level)) == NULL)
	{
	  fblog.level = 0;
	  break;
	}

      n = fblog.level - (p + 1 - fblog.buf);
      memmove (fblog.buf, p + 1, n);
      fblog.level = n;
    }
  memcpy (fblog.buf + fblog.level, buf, n);
  fblog.level += n;
  fblog.buf[fblog.level++] = '\n';
}

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;
    }

  log_fd = fd;
  if (connect (fd, (struct sockaddr*)&log_sa, log_salen))
    {
      fallback_log ("can't connect to log socket %s: %s", dev,
		    strerror (errno));
      switch (errno) {
      case EAGAIN:
      case EINTR:
      case ECONNREFUSED:
	break;

      default:
	close (fd);
	log_fd = LOGFD_DISABLED;
      }
      return -1;
    }

  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:
	  if (reopen_logger () == 0)
	    break;
	  /* fall through */

	case LOGFD_DISABLED:
	  fallback_log ("%*.*s", msg->len, msg->len, msg->text);
	  free (msg);
	  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;

      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;
	}

      /* Otherwise, log the fact (for debugging) ... */
      fallback_log ("sendto: %s", strerror (errno));

      /* ... and log to the fallback channel. */
      msg = log_message_dequeue ();
      fallback_log ("%*.*s", msg->len, msg->len, msg->text);
      free (msg);
      /* Close the descriptor. */
      close (log_fd);
      log_fd = LOGFD_NOT_OPEN;
    }

  if (log_queue_length == pies_log_max_queue &&
      log_queue_head->drop_count > pies_log_max_queue)
    /* Drain the queue */
    while ((msg = log_message_dequeue ()) != NULL)
      {
	fallback_log ("%*.*s", msg->len, msg->len, msg->text);
	free (msg);
      }
}

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;
}

void
pies_syslog_reload (void)
{
  fallback_close ();
}
