/*
 * dsyslog - a dumb syslog (e.g. syslog for people who have a clue)
 * Copyright (c) 2008 William Pitcock <nenolod@dereferenced.org>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice is present in all copies.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS 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 COPYRIGHT OWNER OR 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.
 */

#include <glib.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>

#include "dsyslog.h"

struct dsyslog_source_tcp {
	GIOChannel *logchan;
	gint tag;
	GList *children;
	struct sockaddr_storage src;
	gpointer parent;
};

/*
 * Skip a RFC3339 timestamp which FreeBSD syslogd sends incorrectly in a
 * message. Returns pointer past timestamp or unchanged pointer
 * if no timestamp was found.
 *
 * <logcode> should have been parsed before calling this function.
 *     - nenolod.
 */
static gchar *
skip_rfc3339_timestamp(gchar *line)
{
	gchar *t;

	_ENTER;

	t = line;

	/*
	 * Look for a month, by matching character sequences, doing it this way
	 * leads to minimal instruction use on at least x86/x86-64.
	 *
	 * Basically, we switch for the first letter and match like so:
	 *   J -> (an) / (ul)
	 *   F -> (eb)
	 *   M -> (ar) / (ay)
	 *   A -> (pr) / (ug)
	 *   S -> (ep)
	 *   O -> (ct)
	 *   D -> (ec)
	 *
	 * By using a static parse tree, lookups are fast.
	 * (and no, this isn't an original idea by any means.) --nenolod
	 */
	switch (*t)
	{
	case 'J':
	case 'F':
	case 'M':
	case 'A':
	case 'S':
	case 'O':
	case 'D':
		t += 4;
		break;
	default:
		_LEAVE line;
	}

	/* some numbers are padded by a space. */
	if (isspace(*t))
		t++;

	/* skip the number */
	while (isdigit(*t))
                t++;

	if (isspace(*t))
		t++;

	/* do we have a timestamp? */
	if (!isdigit(*t))
	{
		_LEAVE t; /* no. this is the program name! horray! */
	}

	/* bollocks. skip the time stamp too. */
	while (isdigit(*t) || *t == ':')
		t++;

	if (*t == ' ')
		t++;

	if (*t == 'A' || *t == 'P')
		t += 2;

	_LEAVE t;
}

static void
parse_tcp_logline(gchar *line, gint *logcode, gchar **program, gchar **message, gint *pid)
{
	gchar *t;

	_ENTER;

	if (*line != '<') {
		_LEAVE;
	}

	if ((*logcode = atoi(++line)) <= 0) {
		_LEAVE;
	}

	while (*line != '>') {
		if (*line == '\0') {
			_LEAVE;
		}

		line++;
	}
	t = skip_rfc3339_timestamp(++line);
	*program = line = t;

	while (*line != '[' && *line != ':') {
		if (*line == '\0') {
			_LEAVE;
		}

		line++;
	}
	if (*line == '[') {
		*line++ = '\0';
		*pid = atoi(line);
		while (*line != ']') {
			if (*line == '\0') {
				_LEAVE;
			}
			line++;
		}
		line++;
		while (*line != ':') {
			if (*line == '\0') {
				_LEAVE;
			}
			line++;
		}
	} else {
		*line = '\0';
	}
	line++;

	while (!isspace(*line)) {
		if (*line == '\0') {
			_LEAVE;
		}

		line++;
	}
	*message = ++line;

	_LEAVE;
}

static gchar *
get_hostname(struct sockaddr *sa, socklen_t len)
{
	gchar ip[NI_MAXHOST];

	_ENTER;

	getnameinfo(sa, len, ip, NI_MAXHOST, NULL, 0, NI_NUMERICHOST);

	_LEAVE g_strdup(ip);
}

static gboolean
incoming_syslog_line(GIOChannel *source, GIOCondition cond, gpointer data)
{
	gchar line[1024];
	gint logcode = 0;
	gchar *program = NULL, *message = NULL, *ip = NULL;
	gint ret = -1;
	gint fd = g_io_channel_unix_get_fd(source);
	gint pid = 0;
	socklen_t srclen = sizeof(struct sockaddr_storage);
	time_t now = time(NULL);
	char datebuf[400];
	struct tm *tm;
	struct dsyslog_source_tcp *usock = data;

	_ENTER;

	if (cond == G_IO_ERR || cond == G_IO_HUP) {
		struct dsyslog_source_tcp *listener = usock->parent;

		_DEBUG("cond == G_IO_ERR!");

		g_source_remove(usock->tag);
		g_io_channel_unref(source);
		close(fd);
		listener->children = g_list_remove_all(listener->children, usock);
		g_slice_free(struct dsyslog_source_tcp, usock);

		_LEAVE FALSE;
	}

	ret = recv(fd, &line, 1024, 0);

	if (ret <= 0) {
		struct dsyslog_source_tcp *listener = usock->parent;

		_DEBUG("ret == -1!");

		g_io_channel_unref(source);
		close(fd);
		listener->children = g_list_remove_all(listener->children, usock);
		g_slice_free(struct dsyslog_source_tcp, usock);

		_LEAVE FALSE;
	}

	line[ret] = '\0';
	parse_tcp_logline(line, &logcode, &program, &message, &pid);
	ip = get_hostname((struct sockaddr *) &usock->src, srclen);
	tm = localtime(&now);
	strftime(datebuf, sizeof datebuf, "%b %e %T", tm);

	dsyslog_event_dispatch(logcode, datebuf, ip, program, pid, message ? message : "");

	g_free(ip);

	_LEAVE TRUE;
}

static gboolean
incoming_syslog_conn(GIOChannel *source, GIOCondition cond, gpointer data)
{
	socklen_t srclen = sizeof(struct sockaddr_storage);
	gint sock, listenfd;
	struct dsyslog_source_tcp *usock;
	struct dsyslog_source_tcp *listener = (struct dsyslog_source_tcp *) data;

	_ENTER;

	listenfd = g_io_channel_unix_get_fd(source);

	usock = g_slice_new0(struct dsyslog_source_tcp);
	sock = accept(listenfd, (struct sockaddr *) &usock->src, &srclen);

	usock->parent = listener;
	usock->logchan = g_io_channel_unix_new(sock);
	usock->tag = g_io_add_watch(usock->logchan, G_IO_IN | G_IO_HUP, incoming_syslog_line, usock);

	listener->children = g_list_prepend(listener->children, usock);

	_LEAVE TRUE;
}

static void
dsyslog_source_tcp_delete(dsyslog_source_t *src)
{
	struct dsyslog_source_tcp *usock;
	gint fd;
	GList *n, *n2;

	_ENTER;

	usock = src->opaque;
	g_source_remove(usock->tag);
	fd = g_io_channel_unix_get_fd(usock->logchan);
	g_io_channel_unref(usock->logchan);
	close(fd);

	for (n = usock->children, n2 = g_list_next(n); n != NULL; n = n2, n2 = g_list_next(n2)) {
		struct dsyslog_source_tcp *usock2 = n->data;
		gint fd2;

		fd2 = g_io_channel_unix_get_fd(usock2->logchan);
		g_source_remove(usock2->tag);
		g_io_channel_unref(usock2->logchan);
		close(fd2);

		usock->children = g_list_remove_all(usock->children, usock2);
	}

	g_slice_free(struct dsyslog_source_tcp, usock);

	_LEAVE;
}

static void
dsyslog_source_tcp_new(dsyslog_source_t *src)
{
	int sock;
	struct sockaddr_in sa;
	struct hostent *hp;
	struct in_addr *in;
	struct dsyslog_source_tcp *usock;

	_ENTER;

	if (!src->host) {
		_LEAVE;
	}

	if (!src->port) {
		src->port = 514;
	}

	sock = socket(PF_INET, SOCK_STREAM, 0);
	if (sock < 0) {
		perror("logsocket");
		_LEAVE;
	}

	sa.sin_family = AF_INET;
	if ((hp = gethostbyname(src->host)) == NULL) {
		close(sock);
		_LEAVE;
	}

	in = (struct in_addr *)(hp->h_addr_list[0]);
	sa.sin_addr.s_addr = in->s_addr;
	sa.sin_port = htons(src->port);

	if (bind(sock, (struct sockaddr *) &sa, (socklen_t) sizeof sa) < 0) {
		perror("logsocket:bind");
		_LEAVE;
	}

	if (listen(sock, DSYSLOG_SOMAXCONN) < 0) {
		perror("logsocket:listen");
		_LEAVE;
	}

	usock = g_slice_new0(struct dsyslog_source_tcp);
	usock->logchan = g_io_channel_unix_new(sock);
	usock->tag = g_io_add_watch(usock->logchan, G_IO_IN, incoming_syslog_conn, usock);
	src->opaque = usock;
	src->destructor = dsyslog_source_tcp_delete;

	_LEAVE;
}

void
_modinit(void)
{
	_ENTER;

	dsyslog_source_register("tcp", dsyslog_source_tcp_new);

	_LEAVE;
}

void
_modfini(void)
{
	_ENTER;

	dsyslog_source_unregister("tcp");

	_LEAVE;
}
