/* Copyright (C) 1999 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 "ftpcmd.h"
#include "reply.h"

typedef struct overload_info
{
	int pos;
	char *msg;
	int len;
} OVERLOAD_INFO;

extern FTPCMD mainftpcmd[];
extern pid_t *deadlist;

int controlportgotdata(SELECTER *sel, int fd, void *peerdata)
{
	FTPSTATE *peer = (FTPSTATE *)peerdata;
	char *instring = NULL;
	int connclosed = readcmd(peer);
	int exits = FALSE;
	INPUTLINE cmd;

	if (connclosed)
		return(TRUE);
	else 
	{
		if (popcmd(peer, &instring))
		{
			ftp_write(peer, FALSE, 500, REPLY_ONECMDONLY);
			return(TRUE);
		}
		if (instring)	/* we have a command */
		{
			cmd_split(peer, &cmd, instring, mainftpcmd, TRUE, 
				  (peer->loggedin ? peer->cmddisableset : NULL));
			if (!peer->dport)
				if (cmd.command->ftpfunc != ftp_pass)
					shinfo_changeop(instring);
				
			exits = ftp_run(peer, &cmd, "FTP"); 
			freeifnotnull(cmd.parameters);
 			if (!peer->dport)
				shinfo_changeop("idle");
				 			
 			string_clear(&(peer->inbuffer));
 		}	
		return(exits);
	}
}

char *remove_rootcomponent(FTPSTATE *peer, char *filename, char *descript)
{	
	int baselen = strlen(peer->basedir);
	pathname_simplify(filename);
	
	if (strncmp(peer->basedir, filename, baselen) == 0)
		memmove(filename, filename + baselen, strlen(filename) - baselen + 1);
	else
	{
		log_giveentry(MYLOG_INFO, NULL, safe_snprintf("%s(%s) is outside of rootdir(%s) with chroot enabled!", descript, filename, peer->basedir));
		freewrapper(filename);
		return(NULL);
	}
	return(filename);
}

void dochroot(FTPSTATE *peer)
{
	/* this prevents bugs in resolving filenames */
	if (strcmp(peer->basedir, "/") == 0)
	{
		peer->chroot = FALSE;
		return;
	}
	if (config->rootmode)
	{
		int res = chroot(peer->basedir);
		if (res == 0)
		{
			/* put ourselves into the tree */
			chdir("/");

			/* once chroot is done, nothing can undo it,
			   so user cannot relogin */
			peer->jailenabled = TRUE;

			/* now change dumped file names */
			pathname_simplify(peer->basedir);
			if (peer->logindump)
				peer->logindump = remove_rootcomponent(peer, peer->logindump, "welcome");
			if (peer->quitdump)
				peer->quitdump = remove_rootcomponent(peer, peer->quitdump, "quitdump");
			if (peer->cwddump)
				if ((peer->cwddump)[0] == '/')
					peer->cwddump = remove_rootcomponent(peer, peer->cwddump, "cddump");
 
			freewrapper(peer->basedir);
			peer->basedir = strdupwrapper("/");
			
		}
		else
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("chroot('%s') gave error %s", peer->basedir, strerror(errno)));
	}
	else
		log_addentry(MYLOG_INFO, NULL, "Cannot use chroot() without root permission.");
}			

void rotatelogs(FTPSTATE *peer)
{
	char *logfile;
	int newlogcontext;
				 
	if (peer->vserver->logfile)
		logfile = peer->vserver->logfile;
	else
		logfile = config->defaults->logfile;
	
	if ((!peer->chroot) || (!peer->droproot))
	{
		/* use root to open log */
		file_becomeroot(peer);
		newlogcontext = log_initcontext(logfile);
		file_becomeuser(peer);
					
		if (newlogcontext == -1)
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("Could not open log file '%s', reopen failed", peer->vserver->logfile));
		else
		{
			log_shutdown();
			config->logout = newlogcontext;
			log_setcontext(config->logout, peer->vserver->loglevel);
		}
	}
	else
		log_addentry(MYLOG_INFO, NULL, "Cannot reopen log file in chroot or droproot mode");
}			

int vserver_select(FTPSTATE *peer, VSERVER *vserver)
{
	int error, result;
	int newlogcontext;

	peer->vserver = vserver;

	/* initalize the new log context */
	newlogcontext = config->logout;
	
	if (vserver->logfile)
		if (strcmp(config->defaults->logfile, vserver->logfile) != 0)
		{
			newlogcontext = log_initcontext(vserver->logfile);
			if (newlogcontext == -1)
			{
				log_giveentry(MYLOG_INFO, NULL, safe_snprintf("Dropping connection, error opening virtual server logfile '%s': %s", vserver->logfile, strerror(errno)));
				return(1);
			}
			log_shutdown();
			config->logout = newlogcontext;
		}
	
	log_setcontext(config->logout, vserver->loglevel);

	/* see if the user is allowed in this vserver */
	if (!user_allowed(vserver->ipaccess, peer->remoteip, peer->hostname))
	{
		log_addentry(MYLOG_DACCESS, peer, NULL);
		return(2);
	}
	
	/* set the procdata up */
	result = shinfo_setvserver(peer->threadnum, vserver->sectionname, 
				   peer->remoteip, vserver->maxusers,
				   vserver->maxperip, &error);
	if (!result)
	{ 
		logfullmessage(error, peer->remoteip);
		return(3);
	}
	
	return(0);
}

void ftpserverside_main(int remotefd, int remoteip, int threadnum, int portnum, VSERVER *vserver)
{
	int newlogcontext, exits = FALSE;
	int flag = 1;
	struct rlimit mylimits = { MAXMEMUSAGE, MAXMEMUSAGE };
	FTPSTATE *peer = mallocwrapper(sizeof(FTPSTATE));
	char *greetline;
	
	ftpstate_init(peer, remotefd, remoteip, threadnum, portnum, vserver); 
		/* initalize ftp state data struture */
	shinfo_sethost(peer->hostname);

	if (!user_allowed(config->defaults->ipaccess, remoteip, peer->hostname))
	{
		log_addentry(MYLOG_DACCESS, peer, NULL);
		close(remotefd);
		return;
	}

	newlogcontext = config->logout;
	
	if (vserver->logfile)
		if (strcmp(config->defaults->logfile, vserver->logfile) != 0)
		{
			newlogcontext = log_initcontext(vserver->logfile);
			if (newlogcontext == -1)
			{
				log_giveentry(MYLOG_INFO, NULL, safe_snprintf("Dropping connection, error opening virtual server logfile '%s': %s", vserver->logfile, strerror(errno)));
				close(remotefd);
				return;
			}
			log_shutdown();
			config->logout = newlogcontext;
		}
	
	log_setcontext(config->logout, vserver->loglevel);

	setrlimit(RLIMIT_DATA, &mylimits);
	
	peer->vserver = vserver;
	
	/* see if user is allowed to enter */

	if (!user_allowed(vserver->ipaccess, remoteip, peer->hostname))
	{
		log_addentry(MYLOG_DACCESS, peer, NULL);
		close(remotefd);
		return;
	}

	peer->sel = select_new();
	
	setsockopt(peer->remotefd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int));
	
	/* add the remote fd for reading */
	select_addfd(peer->sel, remotefd);
	select_addread(peer->sel, remotefd, controlportgotdata, (void *)peer);
	
	/* output our greeting */
	shinfo_changeop("idle");
	
	if (vserver->greetline)
		greetline = vserver->greetline;
	else
		greetline = REPLY_GREET;

	if (vserver->prelogindumpdata)
		exits = ftp_dumpstr(peer, vserver->prelogindumpdata, 220, greetline, TRUE);
	else
		exits = ftp_dumper(peer, nfopen(vserver->prelogindump), 220, greetline, TRUE, TRUE);

 	while(!exits)
 	{
		int fd, signalnum;
		
 		fd = select_do(peer->sel, &signalnum, peer->timeout);
		/* if we get 3 back, simply loop and update timeout */
 		if (fd == -1)
 		{
			if ((signalnum == SIGTERM) || (signalnum == SIGHUP))
 			{
 				/* server is shutting down! */
 				ftp_write(peer, FALSE, 421, REPLY_SERVERSHUTDOWN);
 				exits = TRUE;
 			}
 			else if (signalnum == SIGUSR1)
			{
				rotatelogs(peer);
			}				
 			else if (signalnum == 0)
 				exits = TRUE;
		
		}
		else if (fd == 0)
 		{
 			/* timeout has occurred */
 			ftp_write(peer, FALSE, 421, REPLY_TIMEOUT(peer->timeout));
 			exits = TRUE;
 		} 
 		else if (fd != 3)
 			exits = TRUE;
 	}

	log_addentry(MYLOG_LOGIN, peer, "User logged out.");
	if (peer->acldata)
		acllist_dest(peer->acldata);
	select_shutdown(peer->sel);
	ftpstate_dest(peer);
}

int inport_bind(int portid, unsigned int bindip, int dodie)
{
	int incon;
	
	if ((portid < 1024) && (getuid() != 0))
	{
		char *error = safe_snprintf("root access required to bind port %d (less than 1024), skipping", portid);
		if (dodie)
			ERRORMSGFATAL(error);
		else
			log_giveentry(MYLOG_INFO, NULL, error);
		return(-1);
	}
	
	incon = listenport(portid, bindip, MAXPENDCONN);

	if (incon == -1)
	{
		char *error = safe_snprintf("Cannot bind to port %d, ip %s, skipping!", portid, getipstr(bindip));
		if (dodie)
			ERRORMSGFATAL(error); 
		else	
			log_giveentry(MYLOG_INFO, NULL, error);
	}
	return(incon);
}

int overload_sendmessage(SELECTER *mainsel, int fd, void *in)
{
	OVERLOAD_INFO *i = (OVERLOAD_INFO *)in;
	int c;
	
	c = write(fd, i->msg + i->pos, i->len);
	
	i->len -= c;
	i->pos += c;
	
	if ((c <= 0) || (i->len <= 0))
	{
		freewrapper(i);
		return(2);
	}
	
	return(FALSE);
}

int inport_getconn(SELECTER *mainsel, int fd, void *vs)
{
	int newconn;
	unsigned int ipaddress, lip;
	VSERVER *vserver;
	int tnum, port, error, result;
	pid_t fresult;
	
	newconn = get_conn(fd, &ipaddress); 
	if (newconn == -1)
	{
		log_giveentry(MYLOG_INFO, NULL, safe_snprintf("Error accepting control connection: %s", strerror(errno)));
		return(FALSE);
	}

	if (config->hostvservers)
	{
		getsockinfo(newconn, &lip, &port);
		vserver = config->defaults;
	}
	else if (vs != NULL)
	{
		vserver = ((VSERVERCONN *)vs)->vptr;
		port = ((VSERVERCONN *)vs)->port;
	}
	else
	{
		getsockinfo(newconn, &lip, &port);
		vserver = findvserver(lip, port);
	}

	/* was not able to find a vserver for the port/ip they connected to */
	if (vserver == NULL)
	{
		close(newconn);
		return(FALSE);
	}
	
	result = TRUE;
	tnum = shinfo_newuser_standalone(ipaddress, config->defaults->maxperip, &error);
	if ((tnum != -1) && (config->vservers) && (!config->hostvservers))
		result = shinfo_setvserver(tnum, vserver->sectionname, ipaddress,
					   vserver->maxusers,
					   vserver->maxperip,
					   &error);
	
	if ((tnum != -1) && (result))
	{
		fresult = fork();
		if ((int)fresult == 0)
		{	
			shinfo_setpid(tnum, (int)getpid());
			select_shutdown(mainsel);
			freewrapper(deadlist);
			ftpserverside_main(newconn, ipaddress, tnum, port, vserver);
			ftpd_killconfig(config);
			kill_uidgidfiles();
			log_shutdown();
			exit(0);
		}
		else if ((int)fresult == -1)
		{
			shinfo_freebynum(tnum);
			log_addentry(MYLOG_INFO, NULL, "Could not fork, dropping connection.");
			ERRORMSG("Could not fork, dropping connection!");
			close(newconn);
		}
		else
		{
			close(newconn);
		}
	}
	else 
	{
		/* server is full, or too many connections */
		if (tnum != -1)
			shinfo_freebynum(tnum);
		logfullmessage(error, ipaddress);
		if (config->toobusycount <= MAXTOOMANYUSERS)
		{
			OVERLOAD_INFO *i = mallocwrapper(sizeof(OVERLOAD_INFO));
			
			i->msg = vserver->toobusy;
			if (i->msg == NULL)
				i->msg = REPLY_SERVERBUSY;
			i->pos = 0;
			i->len = strlen(i->msg);
			fcntl(newconn, F_SETFL, O_NONBLOCK);
			select_addfd(mainsel, newconn);
			select_addwrite(mainsel, newconn, overload_sendmessage, i);
		}
		else
			close(newconn);
	}
	return(FALSE);
}
