/* 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"

/* these are used when the server is run in standalone mode. In standalone
   mode shared memory is used to hold user info */

SHRMEMHEADER *shdata = NULL;	/* The start of the shared memory, containing
				   the vserver, group records */ 
SCRMEMREC *procdata = NULL;	/* start of array holding thread info. This
				   is all in memory since it is much faster. */
int nextthid = -1;		/* the next free thread id in the list */
int shlockfile = -1;		/* file fd used with fcntl file locking to
				   control access to the shared memory.
				   This is typically the config file */
int shnumber = -1;		/* the number allocated to the shared memory
				   area. */
				   
/* these are used when the server is run in inetd mode */

int scratchfile = -1;		/* The fd of the scratchfile. The scratchfile
				   is used to co-ordinate group limits. */

/* used by both modes */

int thid = -1;			/* The thread id of the current muddleftpd
				   client */


/* these simplify writing to the process areas */

void writescratch(int pos, int size, char *buff)
{
	lseek(scratchfile, pos, SEEK_SET);
	write(scratchfile, buff, size);
}

void writeshmem(int num, int pos, int len, void *buff)
{
	lockarea(shlockfile, 10+num, 1, F_WRLCK, TRUE);

	/* messy. Basicly num is added to procdata as a SCRMEMREC pointer
	   then pos is added as a char pointer */
	memcpy((char *)(procdata + num) + pos, buff, len);
	lockarea(shlockfile, 10+num, 1, F_UNLCK, TRUE);
}

void write_procstr(int cthid, int shmem_pos, int file_pos, int length,
		   char *strbuff)
{
	char endchr = 0;
	int writelen = strlen(strbuff) + 1;
	
	/* if string is too long, null terminate it! */
	if (writelen >= length)
	{
		endchr = strbuff[length-1];
		strbuff[length-1] = 0;
		writelen = length;
	}
	
	if (inetd)
		writescratch(cthid * sizeof(SCRFILEREC) + file_pos, 
			     writelen, strbuff);
	else
		writeshmem(cthid, shmem_pos, writelen, strbuff);
	
	if (endchr != 0)
		strbuff[length-1] = endchr;
}

void shinfo_addtogrouplist(VSERVER *vs)
{
	SHRMEMDATA *dpos;
	int gpos = 0;
	int count, flag, section, maxcount;
	
	while(vs->grouplist[gpos] != NULL)
	{
		/* check to see if the group is already in the list */
		flag = FALSE;
		dpos = SHRMEMDATPOS(shdata, shdata->numvserver);
		for(count = 0; count < shdata->numgroups; count++)
		{
			if (strncmp(vs->grouplist[gpos], dpos->name, MAXSECTIONLEN) == 0)
				flag = TRUE;
			dpos++;
		}
		
		if (!flag)
		{
			/* get maxuser information */
			section = getsectionid(config->configfile, vs->grouplist[gpos]);
			loadintfromconfig(config->configfile, section, "maxusers", &maxcount, config->defaults->maxusers);
			/* A group with 0 maxusers doesn't get added! */
			if (maxcount == 0)
				flag = TRUE;
		}
			
		if (!flag)
		{
			/* add the group, dpos is already initalized from above
			   loop */
			strncpy(dpos->name, vs->grouplist[gpos], MAXNAMLEN);
			dpos->name[MAXSECTIONLEN-1] = 0;
			dpos->max = maxcount;
			dpos->count = 0;
			shdata->numgroups++;
		}
		gpos++;
	}
}		
			
void shinfo_setuparea(void)
{
	int count = 0;
	SHRMEMDATA *vpos;
	VSERVER *vs;
	
	shdata->magic = CURRENTMAGIC;
	shdata->serverusercount = 0;
	shdata->servermaxcount = config->defaults->maxusers;
	shdata->numvserver = 0;
	shdata->numgroups = 0;
	shdata->pid = getpid();

	vpos = SHRMEMDATPOS(shdata, 0);
	vs = config->vservers;
	
	while(config->vserverlist[count] != NULL)
	{
		vpos->count = 0;
		vpos->max = vs->maxusers;
		strncpy(vpos->name, config->vserverlist[count], MAXSECTIONLEN);
		vpos->name[MAXSECTIONLEN-1] = 0;
		vpos++;
		vs = vs->next;
		shdata->numvserver++;
		count++;
	}

	vs = config->vservers;
	if (!vs)
		shinfo_addtogrouplist(config->defaults);
	else
		while(vs)
		{
			shinfo_addtogrouplist(vs);
			vs = vs->next;
		}

	if (((config->defaults->maxusers * sizeof(SCRMEMREC)) >
	    (SHMEMSIZE - SHMEMPROCSTART)) || ((shdata->numvserver +
	    shdata->numgroups) * sizeof(SHRMEMDATA) + sizeof(SHRMEMHEADER)
	    > SHMEMPROCSTART))
	    	ERRORMSGFATAL("Not enough shared memory to support configuration.");
}

void shinfo_init(char *scfilename)
{
	int count, new;
	
	scratchfile = open(scfilename, O_RDWR | O_CREAT, 0600);
	if (scratchfile == -1)
		ERRORMSGFATAL("Could not open scratch file!");
	
	shdata = (SHRMEMHEADER *)shmem_get(scfilename, SHMEMSIZE, &shnumber, &new, &shlockfile);
	
	/* attempt to lock the shared area to say I am home */
	
	if (!lockarea(shlockfile, 3, 1, F_WRLCK, FALSE))
		ERRORMSGFATAL("Muddleftpd already running!");
	
	/* make sure no-body else gets in */
	lockarea(shlockfile, 0, 3, F_WRLCK, TRUE);
	shinfo_setuparea();
	lockarea(shlockfile, 0, 3, F_UNLCK, TRUE);
	
	procdata = (SCRMEMREC *)((char *)(shdata) + SHMEMPROCSTART);

	/* initalize pidmapping list. stores free list as negative values */
	for (count = 0; count < config->defaults->maxusers; count++)
	{
		procdata[count].pid = -(count + 1);
		strcpy(procdata[count].username, "none");
		procdata[count].vserver = -1;
		procdata[count].group = -1;
		procdata[count].ip = 0;
	}
	
	shdata->numrecs = config->defaults->maxusers;
	
	/* set the start pointer to the first thread */	
	nextthid = 0;
}

void inetd_init(char *scfile)
{
	scratchfile = open(scfile, O_RDWR | O_CREAT, 0600);
	if (scratchfile == -1)
		ERRORMSGFATAL("Could not open scratch file!");
}

/* this reinitalizes the shared memory area, when config file is updated */

void shinfo_reinit(void)
{
	int count, count2;
	SHRMEMHEADER *old = mallocwrapper(SHMEMPROCSTART);
	SHRMEMDATA *datnew, *datold;
	
	lockarea(shlockfile, 0, 3, F_WRLCK, TRUE);
	lockarea(shlockfile, 10, shdata->numrecs, F_WRLCK, TRUE);
	memcpy(old, shdata, sizeof(SHRMEMHEADER) + sizeof(SHRMEMDATA) * 
			    (shdata->numvserver + shdata->numgroups));
	
	/* reinitalize area */
	shinfo_setuparea();
	
	/* move over old data */
	shdata->serverusercount = old->serverusercount;
	shdata->numrecs = old->numrecs;

	/* update vservers */
	datnew = SHRMEMDATPOS(shdata, 0);
	for(count = 0; count < shdata->numvserver; count++)
	{
		datold = SHRMEMDATPOS(old, 0);
		for(count2 = 0; count2 < old->numvserver; count2++)
		{
			if (strncmp(datnew->name, datold->name, MAXSECTIONLEN) == 0)
				datnew->count = datold->count;
			datold++;
		}
		datnew++;
	}

	for(count = 0; count < shdata->numgroups; count++)
	{
		datold = SHRMEMDATPOS(old, old->numvserver);
		for(count2 = 0; count2 < old->numgroups; count2++)
		{
			if (strncmp(datnew->name, datold->name, MAXSECTIONLEN) == 0)
				datnew->count = datold->count;
			datold++;
		}
		datnew++;
	}

	/* now update all vserver and group id numbers */
	for (count = 0; count < shdata->numrecs; count++)
	{
		char *vserver, *group;
		if ((procdata[count].vserver != -1) && (shdata->numvserver > 0))
		{
			datold = SHRMEMDATPOS(old, procdata[count].vserver);
			vserver = datold->name;
			datnew = SHRMEMDATPOS(shdata, 0);
			procdata[count].vserver = -1;
			for (count2 = 0; count2 < shdata->numvserver; count2++)
			{
				if (strncmp(vserver, datnew->name, MAXSECTIONLEN) == 0)
					procdata[count].vserver = count2;
				datnew++;
			}
		}	
		else
			procdata[count].vserver = -1;
		
		if (procdata[count].group >= 0)
		{
			datold = SHRMEMDATPOS(old, old->numvserver + procdata[count].group);
			group = datold->name;
			datnew = SHRMEMDATPOS(shdata, shdata->numvserver);
			/* say the user is orphaned */
			procdata[count].group = -2;
			for (count2 = 0; count2 < shdata->numgroups; count2++)
			{
				if (strncmp(group, datnew->name, MAXSECTIONLEN) == 0)   
					procdata[count].group = count2;
				datnew++;
			}
		}
	}
			
	if (shdata->servermaxcount > shdata->numrecs)
	{
		for (count = shdata->numrecs; count < shdata->servermaxcount; count++)
		{
			procdata[count].pid = -(count + 1);
			strcpy(procdata[count].username, "none");
			procdata[count].vserver = -1;
			procdata[count].group = -1;
			procdata[count].ip = -1;
		}
			
		shdata->numrecs = shdata->servermaxcount;
	}
	
	lockarea(shlockfile, 0, 3, F_UNLCK, TRUE);
	lockarea(shlockfile, 10, old->numrecs, F_UNLCK, TRUE);
	freewrapper(old);
}

/* set the vserver for a logged in user. REQUIRED for HOST command
   support */

int shinfo_setvserver_standalone(int cthid, char *vservername, unsigned int ip, 
				 int viplimit, int *error)
{
	int pos = 0;
	int done = 0;
	SHRMEMDATA *d = SHRMEMDATPOS(shdata, 0);
	int vipcount = 0;
	
	/* lock vserver-group counts */
	*error = VSERVERFULL;
	
	lockarea(shlockfile, 0, 2, F_WRLCK, TRUE);
	while((pos < shdata->numvserver) && (!done))
	{
		if (strncmp(vservername, d->name, MAXSECTIONLEN) == 0)
			if (d->count < d->max)
				/* found it all */
				done = TRUE;
		if (!done)
		{
			pos++;
			d++;
		}
	}
	
	if ((done) && (viplimit > 0))
	{
		/* safe as long as IP is not set elsewhere */
		int count;

		for (count = 0; count < shdata->numrecs; count++)
		{
			lockarea(shlockfile, 10 + count, 1, F_WRLCK, TRUE);
			if ((procdata[count].ip == ip) && 
			    (procdata[count].vserver == pos))
				vipcount++;
			lockarea(shlockfile, 10 + count, 1, F_UNLCK, TRUE);
		}
		if (vipcount >= viplimit)
		{
			*error = IPVSERVERFULL;
			done = FALSE;
		}
	}

	if (done)
	{
		lockarea(shlockfile, 10 + cthid, 1, F_WRLCK, TRUE);
		procdata[thid].vserver = pos;
		d->count++;
		lockarea(shlockfile, 10 + cthid, 1, F_UNLCK, TRUE);		
 	}
	
	lockarea(shlockfile, 0, 2, F_UNLCK, TRUE);
	return(done);
}				

int shinfo_setvserver_inetd(int newthid, char *vservername, unsigned int ip, 
                            int vlimit, int viplimit, int *error)
{
	int pos;
	int vipcount;
	int vcount;
	int done;
	SCRFILEREC d;

	pos = 0; vcount = 0; vipcount = 0; done = FALSE;
		
	/* grab general scratchfile lock */
		
	lockarea(scratchfile, 0, 1, F_WRLCK, TRUE);
	lseek(scratchfile, 0, SEEK_SET);
                    	
	while(read(scratchfile, &d, sizeof(SCRFILEREC)) == sizeof(SCRFILEREC))
	{
		/* don't count ourselves */
		if (pos == thid)
			pos++;
		else if (lockarea(scratchfile, 10 + pos, 1, F_WRLCK, FALSE))
		{
			/* if locking successed, no-one is home, unlock it */
			lockarea(scratchfile, 10 + pos, 1, F_UNLCK, TRUE);
		}
		else
		{
			if (strncmp(d.vserver, vservername, MAXSECTIONLEN) == 0)
			{
				vcount++;
				if (d.ip == ip)
					vipcount++;
			}
			pos++;
		}
	}

	if ((viplimit > 0) && (vipcount >= viplimit))
		*error = IPVSERVERFULL;
	else if (vcount >= vlimit)
		*error = VSERVERFULL;
	else
	{
		write_procstr(newthid, 0, SCRF_VSERVER, MAXSECTIONLEN, 
			      vservername);
		done = TRUE;
	}
	
	lockarea(scratchfile, 0, 1, F_UNLCK, TRUE);
	return(done);
}

int shinfo_setvserver(int cthid, char *vservername, unsigned int ip,
		      int vlimit, int viplimit, int *error)
{
	if (inetd)
		return(shinfo_setvserver_inetd(cthid, vservername, ip, vlimit,
						viplimit, error));
	else
		return(shinfo_setvserver_standalone(cthid, vservername, ip, 
						    viplimit, error));
}

int shinfo_newuser_standalone(unsigned int ip, int iplimit, int *error)
{
	int tempnextthid, done = FALSE;
	
	lockarea(shlockfile, 0, 2, F_WRLCK, TRUE);
	
	/* see if we have space */
	if (shdata->serverusercount >= shdata->servermaxcount)
	{
		thid = -1;
		*error = HOSTFULL;
	}
	else
	{
		thid = nextthid;
		tempnextthid = -procdata[thid].pid;
		done = TRUE;
	}

	if ((done) && (iplimit > 0))
	{
		/* safe as long as IP is not set elsewhere (other than below) */
		int count;
		int ipcount = 0;

		for (count = 0; count < shdata->numrecs; count++)
		{
			if (procdata[count].ip == ip)
				ipcount++;
		}
		if (ipcount >= iplimit)
		{
			done = FALSE;
			*error = IPHOSTFULL;
		}
	}
		   
	if (done)
	{
		/* increment server count now login is confirmed */
 		shdata->serverusercount++;

		/* update shared memory */
		lockarea(shlockfile, 10 + thid, 1, F_WRLCK, TRUE);
		strcpy(procdata[thid].username, "<unknown>");
		procdata[thid].group = -1;
		procdata[thid].vserver = -1;
		strcpy(procdata[thid].currentop, "none");
		strcpy(procdata[thid].remotehost, "unknown");
		procdata[thid].pid = -1;
		procdata[thid].ip = ip;
		lockarea(shlockfile, 10 + thid, 1, F_UNLCK, TRUE);		

		/* now move the thids along */
		nextthid = tempnextthid;
 	}
	else
		thid = -1;

	lockarea(shlockfile, 0, 2, F_UNLCK, TRUE);
	return(thid);
}

int shinfo_adduser_inetd(unsigned int ip, int slimit, int iplimit, int *error)
{
	SCRFILEREC d;
	int scount, ipcount, pos, full;
	
	/* we are running inetd. go through scratch file, find an
	   empty record, and count space in file. */
	pos = 0; scount = 0; thid = -1;
	full = FALSE;
	
	/* grab general scratchfile lock */
		
	lockarea(scratchfile, 0, 1, F_WRLCK, TRUE);
	lseek(scratchfile, 0, SEEK_SET);

	while(read(scratchfile, &d, sizeof(SCRFILEREC)) == sizeof(SCRFILEREC))
	{
		if (lockarea(scratchfile, 10 + pos, 1, F_WRLCK, FALSE))
		{
			/* if no allocated thid yet, assign it, otherwise
			   unlock free area */
			if (thid == -1)
				thid = pos;
			else
				lockarea(scratchfile, 10 + pos, 1, F_UNLCK, TRUE);
		}
		else
		{
			if (d.ip == ip)
				ipcount++;
			scount++;
		}
		pos++;
	}	 

	if (thid == -1)
	{
		thid = pos;
		lockarea(scratchfile, 10 + pos, 1, F_WRLCK, TRUE);
	}
		
	if (scount >= slimit)
	{
		*error = HOSTFULL;
		full = TRUE;
	}
	
	if ((iplimit > 0) && (ipcount >= iplimit))
	{
		*error = IPHOSTFULL;
		full = TRUE;
	}
	
	if (full)
	{
		lockarea(scratchfile, 10 + pos, 1, F_UNLCK, TRUE);
		lockarea(scratchfile, 0, 1, F_UNLCK, TRUE);
		return(-1);
	}
		
	/* need to add data to file */
	memset(&d, 0, sizeof(SCRFILEREC));
	strcpy(d.username, "<unknown>");
	strcpy(d.groupname, "none");
		
	strncpy(d.vserver, "none", MAXSECTIONLEN);
	/* null terminate d.vserver */
	d.vserver[MAXSECTIONLEN-1] = 0;
		
	strcpy(d.currentop, "none");
	strcpy(d.remotehost, "unknown");
	d.pid = (int)getpid();
	d.ip = ip;

	writescratch(thid * sizeof(SCRFILEREC), sizeof(SCRFILEREC), (char *)&d);
	lockarea(scratchfile, 0, 1, F_UNLCK, TRUE);
	return(thid);
}

void shinfo_changeop(char *operation)
{
	write_procstr(thid, MAXNAMELEN, SCRF_CURRENTOP, DESCRIPLEN, operation);
} 

void shinfo_changeuser(char *username)
{
	write_procstr(thid, 0, 0, MAXNAMELEN, username);
}

void shinfo_sethost(char *hostname)
{
	write_procstr(thid, MAXNAMELEN + DESCRIPLEN, SCRF_REMOTEHOST,
		      MAXNAMELEN, hostname);
} 

int shinfo_addusergroup(char *groupname, int limit)
{
	int count = 0;
	int pos = 0;
	int done = FALSE;
	SHRMEMDATA *d;
	SCRFILEREC da;
	
	if (inetd)
	{
		/* must find count of users in group */
		lockarea(scratchfile, 0, 1, F_WRLCK, TRUE);
		pos = 0;

		lseek(scratchfile, 0, SEEK_SET);
		while(read(scratchfile, &da, sizeof(SCRFILEREC)) == sizeof(SCRFILEREC))
		{
			if (pos != thid)
			{
				if (!lockarea(scratchfile, 10 + pos, 1, F_WRLCK, FALSE))
				{
					if (strncmp(da.groupname, groupname, MAXSECTIONLEN) == 0)
						count++;
				}
				else
					lockarea(scratchfile, 10 + pos, 1, F_UNLCK, TRUE);
			}
			pos++;
		}	 
		
		done = (count < limit);
		if (done)
		{
			/* update scratchfile */
			write_procstr(thid, 0, SCRF_GROUPNAME, MAXSECTIONLEN, groupname);
			count++;
		}
		lockarea(scratchfile, 0, 1, F_UNLCK, TRUE);
	}
	else
	{
		/* non-inetd stuff */
		lockarea(shlockfile, 0, 1, F_WRLCK, TRUE);
		lockarea(shlockfile, 2, 1, F_WRLCK, TRUE);

		d = SHRMEMDATPOS(shdata, shdata->numvserver);
	
		/* find the group in the list */
		
		while((pos < shdata->numgroups) && (!done))
		{
			if (strncmp(groupname, d->name, MAXSECTIONLEN) == 0)
				if (d->count < d->max)
				{
					/* found it all */
					d->count++;
					count = d->count;
					done = TRUE;
				}
			pos++;
			d++;
		} 
		
		if (done)
		{
			lockarea(shlockfile, 10 + thid, 1, F_WRLCK, TRUE);
			if (procdata[thid].group != -1)
				ERRORMSG("ack, inconsistency found!");
			procdata[thid].group = pos - 1;
			lockarea(shlockfile, 10 + thid, 1, F_UNLCK, TRUE);	
		}
		lockarea(shlockfile, 2, 1, F_UNLCK, TRUE);
		lockarea(shlockfile, 0, 1, F_UNLCK, TRUE);
	}
	if (done)
		return(count);
	else
		return(-1);
	
}

void shinfo_setpid(int thrid, int pid)
{
	if (!inetd)
		procdata[thrid].pid = pid;
	else
		writescratch(thrid * sizeof(SCRFILEREC) + SCRF_PID, 
			     sizeof(int), (char *)&pid);
}

void shinfo_delusergroup(char *groupname)
{
	int pos = 0;
	int done = FALSE;
	SHRMEMDATA *d;

	if (!inetd)
	{
		lockarea(shlockfile, 0, 1, F_WRLCK, TRUE);
		lockarea(shlockfile, 2, 1, F_WRLCK, TRUE);

		d = SHRMEMDATPOS(shdata, shdata->numvserver);
	
		while((pos < shdata->numgroups) && (!done))
		{
			if (strncmp(groupname, d->name, MAXSECTIONLEN) == 0)
			{
				/* found it all */
				d->count--;
				done = TRUE;
			}
			pos++;
			d++;
		}
		lockarea(shlockfile, 10 + thid, 1, F_WRLCK, TRUE);
		procdata[thid].group = -1;
		lockarea(shlockfile, 10 + thid, 1, F_UNLCK, TRUE);

		lockarea(shlockfile, 2, 1, F_UNLCK, TRUE);
		lockarea(shlockfile, 0, 1, F_UNLCK, TRUE);
	}
	else
		/* update scratchfile */
		writescratch(thid * sizeof(SCRFILEREC) + SCRF_GROUPNAME,
			     5, "none");
}

void shinfo_freebynum(int threadnum)
{
	SHRMEMDATA *d;

	/* free and update pointers */
	lockarea(shlockfile, 0, 3, F_WRLCK, TRUE);
	lockarea(shlockfile, 10 + threadnum, 1, F_WRLCK, TRUE);
	if (procdata[threadnum].group >= 0)
	{
		d = SHRMEMDATPOS(shdata, shdata->numvserver + procdata[threadnum].group);
		d->count--;
	}
	shdata->serverusercount--;
	if ((shdata->numvserver > 0) && (procdata[threadnum].vserver >= 0))
	{
		d = SHRMEMDATPOS(shdata, procdata[threadnum].vserver);
		d->count--;
	}
	
	procdata[threadnum].group = -1;
	procdata[threadnum].vserver = -1;
	procdata[threadnum].pid = -nextthid;
	procdata[threadnum].ip = -1;
	nextthid = threadnum;
		
	lockarea(shlockfile, threadnum + 10, 1, F_UNLCK, TRUE);
	lockarea(shlockfile, 0, 3, F_UNLCK, TRUE);
}

void shinfo_freethreads(int freecount, pid_t *freelist)
{
	int count = 0;
	int listcount;

	/* sort the freelist */
	
	for(listcount = 0; listcount < freecount; listcount++)
	{	
		count = 0;
		while(count < shdata->numrecs)
		{
			/* pid is safe since parent process is the only 
			   modifier */
			if (procdata[count].pid == freelist[listcount])
				shinfo_freebynum(count);
								
			count++;
		}
	}
}

void pnums_signalchildren(int signalnum)
{
	int count;
	for (count = 0; count < shdata->numrecs; count++)
		if (procdata[count].pid > 0)
	  		kill((pid_t)procdata[count].pid, signalnum);
}
	
void shinfo_shutdown(void)
{
	if (!inetd)
	{
		/* kill the children kindly */
		pnums_signalchildren(SIGTERM);
		shmem_finish(shnumber);
		close(shlockfile);
	}
	else
		close(scratchfile);
}
