/*
 * Ample - An MP3 lender
 *
 * (c) Copyright - David Härdeman <david@2gen.com> - 2001
 *
 */

/*
 * This file is part of Ample.
 *
 * Ample 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 2 of the License, or
 * (at your option) any later version.
 *
 * Ample 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 Ample; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

/*
 * $Id: ample.c,v 1.50 2003/11/25 09:28:27 alphix Exp $
 *
 * This file contains functions to do initial setup (set up sockets,
 * daemonize, prepare log files etc). The main server process (that
 * forks off children to deal with clients) should be executing within
 * this file most of the time.
 */


#include <config.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#ifdef HAVE_SYSLOG_H
#include <syslog.h>
#endif
#ifdef HAVE_LIBWRAP
#include <tcpd.h>
int allow_severity = LOG_INFO;
int deny_severity = LOG_WARNING;
#endif


#include "ample.h"
#include "entries.h"
#include "client.h"
#include "configuration.h"
#include "helper.h"


/* Number of clients currently connected only main proc should change this */
static volatile int num_clients = 0;
/* Global configuration data */
struct global_config gconf;
/* Used for error checking */
extern int errno; 
/* Used to keep track of child status */
struct childstat *childarray;


/* 
 * Creates a socket and sets it to listen on TCP port defined in gconf.port.
 *
 * Arguments: none
 *
 * Returns: the socket that was created
 */
static int 
opentcpconn() 
{
	struct sockaddr_in address;
	int opt = 1;
	int sock;
	
	if ((sock = socket(PF_INET,SOCK_STREAM, 0)) < 0)
		die("failed to open tcp socket\n");
	
	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	address.sin_family = AF_INET;
	address.sin_port = htons((uint16_t)gconf.port);
	memset(&(address.sin_addr), 0, sizeof(address.sin_addr));
	
	if(bind(sock, (struct sockaddr *)&address, sizeof(struct sockaddr_in)))
		die("failed to bind tcp socket\n");
	
	if(listen(sock,5))
		die("failed to listen to tcp socket\n");

	debug(1, "Opened TCP socket, port %d\n", gconf.port);
	return sock;
}


/* 
 * Creates a UDP socket that is used for status messages from clients
 * to server.
 *
 * Arguments: none
 *
 * Returns: the socket that was created
 */
static int 
openudpconn() 
{
	struct sockaddr_in address;
	int sock;
	int socklen = sizeof(struct sockaddr_in);

	if ((sock = socket(PF_INET,SOCK_DGRAM, 0)) < 0)
		die("failed to open udp socket\n");
	
	memset(&(address.sin_addr), 0, sizeof(address.sin_addr));
	address.sin_family = AF_INET;
	address.sin_port = 0; /* any port */
	address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);

	if(bind(sock, (struct sockaddr *)&address, socklen))
		logmsg("failed to bind udp socket\n");
	
	if(getsockname(sock, (struct sockaddr *)&address, &socklen) < 0 )
		logmsg("failed to get name of udp socket\n");

	if(connect(sock, (struct sockaddr *)&address, socklen))
		logmsg("failed to connect udp socket\n");
	
	debug(1, "Opened UDP socket, port %d\n", ntohs(address.sin_port));
	return sock;
}


/*
 * Get's the UDP status message from client.
 *
 * Arguments: udpsocket - the socket to recieve on
 *
 * Returns: void
 */
static void 
getudpmessage(int udpsock)
{
	char buffer[1024];
	char *tmp;
	int childpid;
	int i;

	/* Make sure buffer is NULL terminated */
	memset(buffer, '\0', sizeof(buffer));
	if(recv(udpsock, buffer, sizeof(buffer) - 1, 0) < 0) {
		debug(1, "recv failed\n");
		return;
	}

	tmp = buffer;
	while(*tmp != '\0' && *tmp != ':')
		tmp++;
	if(*tmp == '\0') {
		debug(1, "Erroneous status message from client!\n");
		return;
	}
	
	*tmp = '\0';
	tmp++;
	childpid = strtol(buffer, NULL, 10);
	if(childpid < 1) {
		debug(1, "Erroneous status message from client!\n");
		return;
	}
	
	for(i = 0; i < gconf.max_clients; i++) {
		if(childarray[i].childpid == childpid) {
			if(childarray[i].status)
				free(childarray[i].status);
			childarray[i].status = strdup(tmp);
			debug(1, "Status message from %i: %s\n", childpid, tmp);
			break;
		}
	}
}


/*
 * Reaps the exit status of any child process (subservers), and changes 
 * client count.
 *
 * Arguments: signal - the signal that called this function (SIGCHLD)
 *
 * Returns: void
 */
static void 
sigchild_handler(int signal) 
{
	int i;
	pid_t pid;

	num_clients--;
	pid = wait(&i);
	for(i = 0; childarray[i].childpid != pid; i++);
	childarray[i].childpid = 0;
	if(childarray[i].status)
		free(childarray[i].status);
	if(childarray[i].client)
		free(childarray[i].client);

	debug(1, "child with pid %u exited, currently %d/%d client(s)\n", pid, num_clients, gconf.max_clients);
	for(i = 0; i < gconf.max_clients; i++)
		if(childarray[i].childpid != 0)
			debug(1, "I have child %u alive with status %s\n", childarray[i].childpid, childarray[i].status);
}


/*
 * Deals with SIGHUP by reopening possible log files.
 *
 * Arguments: signal - the signal that called this function (SIGHUP)
 *
 * Returns: void
 */
static void 
sighup_handler(int signal)
{
#ifndef HAVE_SYSLOG_H
	freopen("/dev/null", "r", stdin);
	freopen(gconf.logfile, "a", stdout);
	freopen(gconf.logfile, "a", stderr);
#endif
	return;
}


/*
 * Checks if a file descriptor is a socket.
 *
 * Arguments: fd - the file descriptor to check
 *
 * Returns: TRUE if fd is a socket, FALSE otherwise
 */
static bool 
issocket(int fd)
{
	int v;
	socklen_t l;

	l = sizeof(int);
	return(getsockopt(fd, SOL_SOCKET, SO_TYPE, (void *)&v, &l) == 0);
}


/*
 * Deamonizes the process by the classical method of double fork, also
 * does some other things which are expected by a nice daemon.
 * NOTE: It does *not* chdir("/"), that must be done later for relative
 *       paths to work.
 *
 * Returns: void
 */
static void 
daemonize() 
{
	int i;
	pid_t pid;

	umask(022);

	if(gconf.trace || gconf.inetd)
		return;
	
	if((pid = fork()) > 0)
		exit(EXIT_SUCCESS);
	else if(pid < 0)
		die("fork");
	
	setsid();

	if((pid = fork()) > 0)
		exit(EXIT_SUCCESS);
	else if(pid < 0)
		die("fork");
}


/*
 * Prepares the proper way for ample to log messages.
 *
 * Returns: void
 */
static void 
preparelog()
{
#ifdef HAVE_SYSLOG_H
	signal(SIGHUP, SIG_IGN);
	if(!gconf.trace) {
		openlog("ample", LOG_CONS | LOG_PID, LOG_FACILITY);
		fclose(stdout);
		fclose(stderr);
	}
#else	
	struct sigaction sa;

	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = sighup_handler;
	sigaction(SIGHUP, &sa, NULL);

	if(!gconf.trace) {
		if(freopen(gconf.logfile, "a", stdout) == NULL ||
		   freopen(gconf.logfile, "a", stderr) == NULL) {
			/* We must fall back to no logging at all */
			fclose(stdout);
			fclose(stderr);
		}
	}
#endif	

	if(!gconf.inetd) {
		fclose(stdin);
	}
	return;
}


/*
 * Checks if the client is allowed to access the server.
 *
 * Arguments: conn - the file descriptor of the socket to check
 *            address - the address struct for the client
 *
 * Returns: TRUE if client is allowed, otherwise FALSE is returned and the
 *          socket is closed.
 */
static bool 
check_access(int conn, struct sockaddr_in *address)
{
#ifdef HAVE_LIBWRAP

	/* Is the client allowed by TCP Wrappers? */
	struct request_info request;
	
	request_init(&request,
		     RQ_DAEMON, "ample", 
		     RQ_CLIENT_SIN, address, 
		     NULL);
	fromhost(&request);
	if (!hosts_access(&request)) {
		logmsg("Connection from %s:%d denied by TCP wrappers\n", 
		       inet_ntoa(address->sin_addr), address->sin_port);
		close(conn);
		return FALSE;
	}

#endif

	/* Not too many clients? */
	if(!gconf.inetd && num_clients >= gconf.max_clients) {
		logmsg("Connection from %s:%d denied, max clients exceeded\n", 
		       inet_ntoa(address->sin_addr), address->sin_port);
		close(conn);
		return FALSE;
	}
	
	/* Everything seems OK */
	if(gconf.inetd)
		logmsg("Connection from %s:%d accepted\n", 
		       inet_ntoa(address->sin_addr), address->sin_port);
	else
		logmsg("Connection from %s:%d accepted, currently %d/%d clients\n", 
		       inet_ntoa(address->sin_addr), address->sin_port, 
		       num_clients + 1, gconf.max_clients);
	
	return TRUE;
}


/*
 * Blocks until a new incoming connection is available.
 *
 * Arguments: address - pointer to address structure to fill in with client info
 *            tcpsock - the TCP socket to watch for incoming connections
 *            udpsock - the UDP socket to watch for incoming messages
 *
 * Returns: the fd of the newly created socket
 */
static int 
accept_connection(struct sockaddr_in *address, int tcpsock, int udpsock)
{
	fd_set readfds;
	int conn;
	int addrlen = sizeof(struct sockaddr_in);
	
	if(address == NULL || tcpsock < 0)
		return FALSE;
	
	while(TRUE) {
		if(!gconf.inetd) {
			FD_ZERO(&readfds);
			FD_SET(tcpsock, &readfds);
			FD_SET(udpsock, &readfds);
			conn = select(max(tcpsock, udpsock) + 1, &readfds,
				      NULL, NULL, NULL);
			if(conn < 0 && errno == EINTR)
				continue;
			else if(conn < 0)
				die("select()\n");
			
			if(FD_ISSET(udpsock, &readfds))
				getudpmessage(udpsock);
			
			if(!FD_ISSET(tcpsock, &readfds))
				continue;
			
			if((conn = accept(tcpsock, (struct sockaddr *)address,
					  &addrlen)) < 0) {
				if(errno == EINTR)
					continue;
				else
					die("accept");
			}

			break;
		
		} else {
			conn = 0;
			getpeername(0, (struct sockaddr *)address, &addrlen);
			break;
		}
	}

	return conn;
}


/*
 * The big schlong.
 * Waits for new client connections and forks a new child to handle 
 * each connection.
 */
int 
main(int argc, char *argv[]) 
{
	struct sockaddr_in address;
	int i, conn, tcpsock, udpsock;
	pid_t pid;
	struct sigaction sa;
	sigset_t chld;
	
	/* Initial setup */
	checkopt(argc, argv);	
	gconf.inetd = issocket(0);
	daemonize();
	preparelog();
	if(!gconf.inetd)
		logmsg("Ample/%s started\n", AMPLE_VERSION);

	/* Prepare child status array */
	childarray = (struct childstat *)
		malloc(gconf.max_clients * sizeof(struct childstat));
	for(i = 0; i < gconf.max_clients; i++) {
		childarray[i].childpid = 0;
		childarray[i].status = NULL;
		childarray[i].client = NULL;
	}

	/* Prepare the virtual file tree of MP3's then we can chdir */
	indexpaths(gconf.pathlist);
	if(!countentries(root))
		die("No files found\n");
	if(chdir("/") < 0)
		die("chdir()\n");

	/* Some debug information */
	debug(1, "Indexed %i file(s)\n", countentries(root));
	showtree(0,root);

	/* Set up signal stuff */
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = sigchild_handler;
	sigaction(SIGCHLD, &sa, NULL);
	sigemptyset(&chld);
	sigaddset(&chld, SIGCHLD);

	/* 
	 * Open, and prepare
	 * UDP socket for status reports
	 * TCP socket for incoming connections
	 */
	if(!gconf.inetd) {
		udpsock = openudpconn();
		tcpsock = opentcpconn();
	} else {
		udpsock = -1;
		tcpsock = 0;
	}
	
 restart:
	/* Accept connection */
	conn = accept_connection(&address, tcpsock, udpsock);

	/* Connection is now set up, check if it's allowed */
	if(!check_access(conn, &address))
		goto restart;

	/* Connection allowed, handle it */
	sigprocmask(SIG_BLOCK, &chld, NULL);
	num_clients++;

	/* Check for special cases */
	if(gconf.trace || gconf.inetd) {
		debug(1, "No-fork/inetd mode: connection from %s:%d\n", 
		      inet_ntoa(address.sin_addr), address.sin_port);
		handleclient(conn, udpsock);
		num_clients--;
		sigprocmask(SIG_UNBLOCK, &chld, NULL);
		return(EXIT_SUCCESS);
	}

	/* This is the regular case, let's fork */
	if((pid = fork()) < 0)
		die("fork()\n");

	if(pid > 0) {
		/* Parent process */
		debug(1, "Connection from %s:%d handled by child with pid %u\n", 
		      inet_ntoa(address.sin_addr), address.sin_port, pid);
		for(i = 0; childarray[i].childpid != 0; i++);

		/* Max length = "255.255.255.255:65536" = 22 */
		if((childarray[i].client = malloc(22)) == NULL)
			die("malloc");
		snprintf(childarray[i].client, 22, "%s:%d", 
			 inet_ntoa(address.sin_addr), address.sin_port);
		childarray[i].status = strdup("Starting");
		childarray[i].childpid = pid;
		
		sigprocmask(SIG_UNBLOCK, &chld, NULL);
		close(conn);
		goto restart;
	} else {
		/* Child process */
		close(tcpsock);
		return(handleclient(conn, udpsock));
	}
	
	/* Time to say goodbye */
	if(!tcpsock)
		close(tcpsock);
	return(EXIT_SUCCESS);
}
