/* $Id$ */
/* Copyright (c) 2011 Pierre Pronchery <khorben@defora.org> */
/* This file is part of DeforaOS Servers inetd */
/* This program 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, version 3 of the License.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. */



#include <sys/types.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "parser.h"
#include "inetd.h"


/* variables */
InetdState * inetd_state;


/* inetd_error */
int inetd_error(char const * message, int ret)
{
	fputs("inetd: ", stderr);
	perror(message);
	return ret;
}


/* inetd */
static int _inetd_init(InetdState * state, char const * filename);
static int _inetd_do(InetdState * state);

static int _inetd(int debug, int queue, char const * filename)
{
	InetdState state;
	int ret;

	inetd_state = &state;
	state.debug = debug;
	state.queue = queue;
	state.filename = filename;
	if(_inetd_init(&state, filename))
		return 2;
	ret = _inetd_do(&state);
	config_delete(state.config);
	return ret ? 2 : 0;
}

static void _inetd_sighandler(int signum);
static int _inetd_daemonize(InetdState * state);
static int _inetd_setup(InetdState * state);
static int _inetd_init(InetdState * state, char const * filename)
{
	struct sigaction sa;

	sa.sa_handler = _inetd_sighandler;
	sigfillset(&sa.sa_mask);
	if(sigaction(SIGCHLD, &sa, NULL) == -1
			|| sigaction(SIGHUP, &sa, NULL) == -1)
		return inetd_error("sigaction", 1);
	if((state->config = parser(filename)) == NULL
			|| _inetd_daemonize(state)
			|| _inetd_setup(state))
		return 1;
	return 0;
}

static void _inetd_sigchld(void);
static void _inetd_sighup(void);
static void _inetd_sighandler(int signum)
{
	switch(signum)
	{
		case SIGCHLD:
			_inetd_sigchld();
			break;
		case SIGHUP:
			_inetd_sighup();
			break;
	}
}

static void _inetd_sigchld(void)
{
	pid_t pid;
	int status;
	unsigned int i;

	if((pid = waitpid(-1, &status, WNOHANG)) == -1)
	{
		inetd_error("waitpid", 0);
		return;
	}
	for(i = 0; i < inetd_state->config->services_nb; i++)
		if(inetd_state->config->services[i]->pid == pid)
		{
			inetd_state->config->services[i]->pid = -1;
			FD_SET(inetd_state->config->services[i]->fd,
					&inetd_state->rfds);
			break;
		}
	if(inetd_state->debug)
		fprintf(stderr, "%s%u%s%s%d%s", "inetd: Child ", (unsigned)pid,
				WIFEXITED(status) ? " exited"
				: " was terminated", " with error code ",
				WEXITSTATUS(status), "\n");
}

static void _inetd_sighup(void)
{
	Config * config;
	unsigned int i;
	unsigned int j;
	Service * sold;
	Service * snew;

	if((config = parser(inetd_state->filename)) == NULL)
	{
		if(inetd_state->debug)
			fprintf(stderr, "%s%s%s", "inetd: ",
					inetd_state->filename,
					"Ignoring reconfiguration request\n");
		return;
	}
	for(i = 0; i < inetd_state->config->services_nb; i++)
	{
		sold = inetd_state->config->services[i];
		for(j = 0; j < config->services_nb; j++)
		{
			snew = config->services[i];
			if(snew->socket != sold->socket
					|| snew->proto != sold->proto
					|| snew->wait != sold->wait
					|| snew->port != sold->port)
				continue;
			snew->fd = sold->fd;
			sold->fd = -1;
			if(inetd_state->debug)
				fprintf(stderr, "%s%s%s", "inetd: Service \"",
						sold->name, "\" kept\n");
			break;
		}
	}
	for(i = 0; i < inetd_state->config->services_nb; i++)
		if(inetd_state->config->services[i]->fd != -1)
			close(inetd_state->config->services[i]->fd);
	config_delete(inetd_state->config);
	inetd_state->config = config;
	_inetd_setup(inetd_state);
}

static int _inetd_daemonize(InetdState * state)
{
	pid_t pid;
	int fd;
	int i;

	if(state->debug)
	{
		fprintf(stderr, "%s", "inetd: Entered debugging mode\n");
		return 0;
	}
	if((pid = fork()) == -1)
		return inetd_error("fork", 1);
	if(pid != 0)
		exit(0);
	if((fd = open("/dev/null", O_RDWR, 0)) == -1)
		return inetd_error("/dev/null", 0);
	for(i = 0; i <= 2; i++)
	{
		close(i);
		dup2(fd, i);
	}
	return 0;
}

static int _inetd_setup(InetdState * state)
{
	unsigned int i;

	state->fdmax = -1;
	FD_ZERO(&state->rfds);
	for(i = 0; i < state->config->services_nb; i++)
	{
		if(state->config->services[i]->pid != -1)
			continue;
		if(state->config->services[i]->fd == -1
				&& service_listen(state->config->services[i]))
			continue;
		FD_SET(state->config->services[i]->fd, &state->rfds);
		if(state->fdmax < state->config->services[i]->fd)
			state->fdmax = state->config->services[i]->fd;
	}
	return 0;
}

static int _inetd_do(InetdState * state)
{
	fd_set rfdstmp;
	sigset_t sigset;
	unsigned int i;
	Service * s;
	int fd;

	sigfillset(&sigset);
	for(rfdstmp = state->rfds;; rfdstmp = state->rfds)
	{
		if(select(state->fdmax+1, &rfdstmp, NULL, NULL, NULL) == -1)
		{
			if(errno != EINTR)
				return inetd_error("select", 2);
			continue;
		}
		sigprocmask(SIG_SETMASK, &sigset, NULL);
		for(i = 0; i < state->config->services_nb; i++)
		{
			s = state->config->services[i];
			if(FD_ISSET(s->fd, &rfdstmp))
			{
				fd = s->fd;
				service_exec(s);
				if(s->pid != -1)
					FD_CLR(s->fd, &state->rfds);
			}
		}
		sigprocmask(SIG_UNBLOCK, &sigset, NULL);
	}
	return 0;
}


/* usage */
static int _usage(void)
{
	fputs("Usage: inetd [-d][-q len] [config file]\n\
  -d	Debugging mode\n\
  -q	Queue length\n", stderr);
	return 1;
}


/* main */
int main(int argc, char * argv[])
{
	int o;
	int debug = 0;
	int queue = 128;
	char const * filename = "/etc/inetd.conf";
	char * p;

	while((o = getopt(argc, argv, "dq")) != -1)
	{
		if(o == 'd')
			debug = 1;
		else if(o == 'q')
		{
			queue = strtol(optarg, &p, 10);
			if(*optarg == '\0' || *p != '\0')
				return _usage();
		}
		else
			return _usage();
	}
	if(argc - optind == 1)
		filename = argv[optind];
	else if(argc != optind)
		return _usage();
	return _inetd(debug, queue, filename) ? 2 : 0;
}
