/* Copyright 2000 Beau Kuiper

   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; either version 2, or (at your option)
   any later version.

   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, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.  */

#include "ftpd.h"
#include "reply.h"

extern int nummalloc;		/* number of areas malloced at once */
int signumber;
pid_t *deadlist;
int deadcount;

void logfullmessage(int error, unsigned int ip)
{
	switch(error)
	{
		case HOSTFULL:
			log_addentry(MYLOG_INFO, NULL, "main maxusers limit reached. Server full");
			break;
		case GROUPFULL:
			log_addentry(MYLOG_INFO, NULL, "group maxusers limit reached. User denied");
			break;
		case VSERVERFULL:
			log_addentry(MYLOG_INFO, NULL, "virtual server maxusers limit reached. User denied");
			break;
		case IPHOSTFULL:
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("main connections per ip reached for %s", getipstr(ip)));
			break;
		case IPVSERVERFULL:
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("virtual server maximum connections per ip reached for %s", getipstr(ip)));
			break;
		default:
			log_addentry(MYLOG_INFO, NULL, "Unknown host error.");
	}
}

VSERVER *findvserver(unsigned int ip, int port)
{
	VSERVERCONN *vs;
	
	vs = config->inports;
	while (vs != NULL)
	{
		if (vs->port == port)
			if ((vs->ip == 0) || (vs->ip == ip))
				return(vs->vptr);
		vs = vs->next;
	}
	return(NULL);
}

/* this function finds all ports binding to multiple ip and uses a single
   bind to use fewer fd's. It runs in N^2 time, but shouldn't be too slow
   even for very large sites. */
   
void smartbind(SELECTER *selset, VSERVERCONN *list, int dodie)
{
	VSERVERCONN *pos, *spos;
	int dobind, fd;
	unsigned int bindip;
	
	pos = list;

	while(pos != NULL)
	{
		/* make sure port hasn't already been bound */
		spos = list;
		dobind = TRUE;	   /* assume the bind goes ahead */
		bindip = pos->ip;  /* assume binding to a single port */
		fd = -1;
		
		/* see if port number is in any ealier binds. If so, then
		   no need to bind this address */
		while((spos != pos) && (dobind))
		{
			if (spos->port == pos->port)
			{
				fd = spos->fd;
				dobind = FALSE;
			}
			spos = spos->next;
		}
		
		if (dobind)
		{
			unsigned int zeroip;
			getnetworkint("0.0.0.0", &zeroip);
			spos = pos->next;
			
			/* now search for binds to this address later in the
			   chain. Set ip to 0.0.0.0 if this is the case */
			while((spos != NULL) && (bindip != zeroip))
			{
				if (spos->port == pos->port)
					bindip = zeroip;
				spos = spos->next;
			}
			/* see if we need to bind to zero instead of
			   bindip */
			
			if ((config->zerobind) && (bindip != zeroip))
			{
				bindip = zeroip;
			}
			/* now actually bind it */

			pos->fd = inport_bind(pos->port, bindip, dodie);
			if (pos->fd != -1)
			{
				select_addfd(selset, pos->fd);
				if (bindip == pos->ip)
					select_addread(selset, pos->fd, inport_getconn, pos);
				else
					select_addread(selset, pos->fd, inport_getconn, NULL);
			}
		}
		else
			pos->fd = fd;
		
		pos = pos->next;
	}	
}

/* This function does binding the dumb way. It runs in N time, but consumes 
   more file descriptors. However, it also doesn't bind to unneeded ip/port
   combinations and is more robust. */

void dumbbind(SELECTER *selset, VSERVERCONN *list, int dodie)
{
	VSERVERCONN *pos = list;
	
	while(pos != NULL)
	{
		pos->fd = inport_bind(pos->port, pos->ip, dodie);
		if (pos->fd != -1)
		{
			select_addfd(selset, pos->fd);
			select_addread(selset, pos->fd, inport_getconn, pos);
		}
		pos = pos->next;
	}
} 

void sighandler(int signum)
{
	signumber = signum;
	debuglog("main got a signal %d", signum);

	if (signum == SIGCHLD)
	{
		int errno2, result; 
		pid_t deadpid;

		debuglog("sighandler - main waiting for child!");		 

		errno2 = errno;
		while((deadpid = waitpid(0, &result, WNOHANG)) > 0)
		{
			debuglog("pid %d died with result %d!", deadpid, result);
			deadlist[deadcount] = deadpid;
			deadcount += 1;
		}
		errno = errno2;

		debuglog("sighandler - main child %d finished!", (int)deadpid);

		signal(SIGCHLD, sighandler);
	}
	else
	if (signum == SIGUSR1)
	{
		signal(SIGUSR1, sighandler);
	}
}

int mainprog(char *fconfig, int runforeground, int verbose)
{
	int signum = 0;
	SELECTER *mainports;
#ifdef RLIMIT_NPROC
	struct rlimit newlimit;
#endif

	test_libc(verbose);
	
	setsid();
	
	signal(SIGIO, SIG_IGN);
	signal(SIGPIPE, SIG_IGN);
	signal(SIGTERM, sighandler);
	signal(SIGHUP, sighandler);
	signal(SIGCHLD, sighandler);
	signal(SIGUSR1, sighandler);
	signal(SIGINT, sighandler);

	logerrors = TERMINAL;		
	
	/* close all file descriptiors */
	fd_closeall_nonterminal();
	
	init_pwgrfiles();
	
	ftpd_init(fconfig, verbose);
	config->toobusycount = 0;
	
	deadlist = mallocwrapper(sizeof(pid_t) * config->defaults->maxusers + 1);
	deadcount = 0;
	
	logerrors = MUDLOG;
	mainports = select_new(); 

	/* Set process limit very high so that the 'poor user' doesn't 
	   need to worry about setting it */

#ifdef RLIMIT_NPROC
	getrlimit(RLIMIT_NPROC, &newlimit);
	newlimit.rlim_cur += config->defaults->maxusers + 1;
	newlimit.rlim_max += config->defaults->maxusers + 1;
	setrlimit(RLIMIT_NPROC, &newlimit);
#endif

	/* Listen to all the input sockets, they are changed to the fd */

	if (config->smartbind)
		smartbind(mainports, config->inports, TRUE);
	else
		dumbbind(mainports, config->inports, TRUE);

	if (((int)config->uidt_asuid != (int)getuid()) || 
	     ((int)config->gidt_asgid != (int)getgid()))
	{
		if ((int)getuid() == 0)
		{
			if (verbose)
				printf("Switching to user '%s' (UID: %d, GID %d)\n", 
					config->username,
					(int)config->uidt_asuid, (int)config->gidt_asgid);

			/* Tell the parent the terminal can be given back! */
			/* It must be done before we change uid because we
			   lose permission to do so after the setuid! */
			if (!runforeground)
				kill(getppid(), SIGHUP); 

			/* Changing user to the one the user wanted */
			setgid(config->gidt_asgid);
			setuid(config->uidt_asuid);
		}
		else
			ERRORMSGFATAL("The server does not have permission to switch to the specified user.");
	}
	else
	{
		if (!runforeground)
			/* Tell the parent the terminal can be given back! */
			kill(getppid(), SIGHUP);
	}
	
	/* close terminal file descriptors */
	
	if (!runforeground)
	{
		close(0); close(1); close(2);
	}
	
	log_addentry(MYLOG_INFO, NULL, PROGNAME" ("VERSTR") server started. Waiting on connections.");

	while ((signum != SIGTERM) && (signum != SIGINT))
	{	
		select_do(mainports, &signum, -1);
		
		blockallsignals();
		if (deadcount > 0)
		{
			shinfo_freethreads(deadcount, deadlist);
			deadcount = 0;
		}
		if ((signum == SIGHUP) && (config->username == NULL))
		{
			/* rebuild configuration */
			CONFIGDATA *newdata;
			int result;
			
			log_addentry(MYLOG_INFO, NULL, "Received SIGHUP, reloading configuration");
			
			newdata = ftpd_loadconfig(fconfig, FALSE, config->defaults->umask);
			
			if (newdata)
				result = ftpd_checkconfig(newdata);

			if ((!newdata) || (!result))
			{
				log_addentry(MYLOG_INFO, NULL, "Errors reloading config file, resuming old config");
				if (newdata)
					ftpd_killconfig(newdata);
			}
			else
			{
				int t = config->toobusycount;
				ftpd_killconfig(config);
				config = newdata;
				config->toobusycount = t;
				log_shutdown();
				log_setcontext(config->logout, config->defaults->loglevel);
				log_addentry(MYLOG_INFO, NULL, "New configuration loaded, using new configuration");
				select_shutdown(mainports);
				mainports = select_new();
				
				if (config->smartbind)
					smartbind(mainports, config->inports, FALSE);
				else
					dumbbind(mainports, config->inports, FALSE);
				shinfo_reinit();
			}
			signal(SIGHUP, sighandler);
		}
		else if ((signum == SIGHUP) && (config->username != NULL))
			log_addentry(MYLOG_INFO, NULL, "You cannot reload the configuration while using the runasuser directive");
		else if (signum == SIGUSR1)
		{
			int newcontext = log_initcontext(config->defaults->logfile);
			if (newcontext == -1)
				log_giveentry(MYLOG_INFO, NULL, safe_snprintf("Could not open logfile '%s', couldn't reopen.", config->defaults->logfile));
			else
			{
				config->logout = newcontext;
				log_shutdown();
				pnums_signalchildren(SIGUSR1);
				log_setcontext(config->logout, config->defaults->loglevel);
				log_addentry(MYLOG_INFO, NULL, "Log file reopened after SIGUSR1.");
			}
		}
		unblockallsignals();

		signal(SIGIO, SIG_IGN);
		signal(SIGHUP, sighandler);
	}

	shinfo_shutdown();

	select_shutdown(mainports);
	                                                 
	log_addentry(MYLOG_INFO, NULL, PROGNAME" ("VERSTR") server shutting down.");	

	freewrapper(deadlist);
	ftpd_killconfig(config);
	log_shutdown();	
	kill_uidgidfiles();
	exit(0);
}

void mainprog_inetd(char *fconfig, unsigned int ip)
{
	char *scratchfile;
	int tnum, port, result;
	unsigned int ipl;
	VSERVER *vserver;
	int error;
	
	test_libc(0);
	
	inetd = TRUE;
	signal(SIGUSR1, sighandler);

	init_pwgrfiles();

	logerrors = SYSLOG;
	ftpd_preinit();
	config = ftpd_loadconfig(fconfig, TRUE, umask(0));
	if (config)
		result = ftpd_checkconfig(config);
	if ((!config) || (!result))
		ERRORMSGFATAL("Errors loading config file, cannot continue!"); 

	log_setcontext(config->logout, config->defaults->loglevel);

	result = getsectionid(config->configfile, "main");
	loadstrfromconfig(config->configfile, result, "scratchfile", 
			  &scratchfile, SCRATCHFILE);
	if (scratchfile[0] != '/')
		ERRORMSGFATAL("Scratchfile is not a valid absolute filename");
		
	ftpd_setnogroups();	
	signal(SIGPIPE, SIG_IGN);
	signal(SIGTERM, sighandler);
	signal(SIGHUP, sighandler);

	inetd_init(scratchfile);

	if (((int)config->uidt_asuid != (int)getuid()) || 
	     ((int)config->gidt_asgid != (int)getgid()))
	{
		if (getuid() == 0)
		{
			setgid(config->gidt_asgid);
			setuid(config->uidt_asuid);
		}
	}
	
	/* get the virtual server the user connected to! */
	
	getsockinfo(0, &ipl, &port);
	vserver = findvserver(ipl, port);
	
	if (vserver == NULL)
		exit(0);
	
	tnum = shinfo_adduser_inetd(ip, config->defaults->maxusers,
				    config->defaults->maxperip, &error);
	
	if (tnum != -1)
		result = shinfo_setvserver(tnum, vserver->sectionname, ip,
					   vserver->maxusers, 
					   vserver->maxperip,
					   &error);
	
	if ((result) && (tnum != -1))
		ftpserverside_main(0, ip, tnum, port, vserver);
	else
	{
		if (vserver->toobusy)
			write(0, vserver->toobusy, strlen(vserver->toobusy));
		else
			write(0, REPLY_SERVERBUSY, strlen(REPLY_SERVERBUSY));
		
		logfullmessage(error, ip);
	}
		
	ftpd_killconfig(config);
	kill_uidgidfiles();

	exit(0);
}

void diehandler(int signum)
{
	exit(0);
}

void usage(char *name)
{
	printf(PROGNAME": ftp daemon.\n\n");
	printf("Usage: %s [-V][-d][-h][-c configfile]\n\n", name);
	printf("	-V		Show version information.\n");
	printf("	-v		Verbose startup.\n");
	printf("	-d		Debug mode. Does not fork into background.\n");
	printf("	-h		Show usage information.\n");
	printf("	-c configfile	Specify an alternative config file.\n\n");	
	exit(1);
}

int main(int argc, char **argv)
{
	unsigned int ip;
	pid_t forkresult;
	int verbose = FALSE;
	int ch;
	int runforeground = FALSE;
	char *fconfig = NULL;
	extern char *optarg;
	
	while((ch = getopt(argc, argv, "Vvc:hd")) != EOF)
	{
		switch(ch)
		{
		case 'V':
			showversion(PROGNAME);
		case 'v':
			verbose = TRUE;
			break;
		case 'c':
			fconfig = optarg;
			break;
		case 'd':
			runforeground = TRUE;
			break;
		case 'h':
		default:
			usage(argv[0]);
		}
		
	}			

	if (fconfig == NULL)
		fconfig = CONFIGFILE;
			
	if ((ip = getremoteip(fileno(stdin))) != 1)
		mainprog_inetd(fconfig, ip);
	
	inetd = FALSE;
	signal(SIGCHLD, diehandler);
	signal(SIGHUP, diehandler); 
	
	if (!runforeground)
	{
		forkresult = fork();
	
		if ((int)forkresult == -1)
			ERRORMSGFATAL("Could not fork into background");
		 
		if ((int)forkresult == 0)
			mainprog(fconfig, FALSE, verbose);
		
		/* wait forever for the child to die or give us a Hangup signal */
	
		while(TRUE)
			pause();
	}
	else
		mainprog(fconfig, TRUE, verbose);

	return(0);
}
