/* auth.c Handle user authentication and setup of user options

   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 "auth.h"

extern FTPCMD mainftpcmd[];
extern FTPCMD siteftpcmd[];

PERMINFO authtypes[] = {
	{ "unix", &unixauth_commands },
	{ "anonymous", &anonauth_commands },
	{ "internal", &internalauth_commands },
	{ "disabled", &disableauth_commands },
#ifdef HAVE_PAM_START
	{ "pam", &pamauth_commands },
#endif
	{ NULL, NULL },
};

/* this converts a config file string to discribe a multiline response to a
   real string. This does not need a new string to copy it to */

void converttorealstr(char *strbuf)
{
	char *pos1, *pos2;
	
	pos1 = pos2 = strbuf;
	
	while(*pos1 != 0)
	{
		if (*pos1 == '/')
		{
			pos1++;
			switch(*pos1)
			{
			case 'n':
				*pos2 = '\n';
				pos2++;
				break;
			case 's':
				*pos2 = ' ';
				pos2++;
				break;
			case 't':
				*pos2 = '\t';
				pos2++;
				break;
			case '/':
				*pos2 = '/';
				pos2++;
				break;
			default:
				*pos2 = '/';
				pos2++;
				pos1--;
				break;
			}	
		}
		else
		{
			*pos2 = *pos1;
			pos2++;
		}
		pos1++;
	}		
	*pos2 = 0;
}

/* check an encrypted password against a plain text password */

int chkpassword(char *encrypass, char *password)
{
	char *pass2;
	int result = FALSE;
	
	pass2 = crypt(password, encrypass);
	if (strcmp(pass2, encrypass) == 0)
		result = TRUE;

	/* destroy the passwords so it cannot be seen if muddleftpd crashes! */
	memset(password, 0, strlen(password));
	memset(pass2, 0, strlen(pass2));
	
	return(result);
}

/* Make sure the user isn't trying to exploit the nature of the ftp server */

int checkexploits(FTPSTATE *peer)
{
	int count;
	int namelen = strlen(peer->username);
	
	if (strlen(peer->username) > MAXNAMELEN)
		return(TRUE);
	for(count = 0; count < namelen; count++)
	{
		switch ((peer->username)[count])
		{
			case '?':
			case '*':
			case '/':
			case '[':
			case ']':
			case '\\':
			case '!':
			case ':':
			case '^':
				return(TRUE);
		}
	}	
	return(FALSE);
}

int authlogmsg(char *directive, char *groupname)
{
	log_giveentry(MYLOG_INFO, NULL, safe_snprintf("directive %s is not correct in group %s", directive, groupname));
	return(FALSE);
}	

int checkabsdir(char *dir)
{
	if (dir == NULL)
		return(TRUE);
	return(dir[0] == '/');
}	
		
void clearauth(FTPSTATE *peer)
{
	freeifnotnull(peer->pwd);
	peer->pwd = NULL;
	freeifnotnull(peer->homedir);
	freeifnotnull(peer->basedir);
	if (peer->passiveport) select_delfd(peer->sel, peer->passiveport);
	peer->passiveport = 0;	
	freeifnotnull(peer->renameoldname);
	peer->renameoldname = NULL;
	peer->restartpos = 0;
	peer->binary = TRUE;
	peer->remoteport = peer->connport - 1;
	freeifnotnull(peer->logindump);
	freeifnotnull(peer->cwddump);
	freeifnotnull(peer->busydump);
	freeifnotnull(peer->quitdump);
	freeifnotnull(peer->logindumpdata);
	freeifnotnull(peer->cwddumpdata);
	freeifnotnull(peer->busydumpdata);
	freeifnotnull(peer->quitdumpdata);
	freeifnotnull(peer->cmddisableset);
	freeifnotnull(peer->sitedisableset);
	peer->sitedisableset = disableset_create();
	peer->cmddisableset = disableset_create();
	peer->umask = peer->vserver->umask;
	if (peer->acldata) acllist_dest(peer->acldata);
	peer->gidt_asgid = config->gidt_nobodygid;
	peer->uidt_asuid = config->uidt_nobodyuid;	
	peer->maxusers = peer->vserver->maxusers;
	peer->maxtimeout = peer->vserver->timeout;
	peer->timeout = peer->vserver->timeout;
	peer->chmodable = FALSE;
	peer->jailenabled = FALSE;
	peer->droproot = FALSE;
	peer->fakemode = -1;
	peer->accessdevices = FALSE;
	peer->fxpallow = FALSE;
	peer->maxtranspd = 0;
	freeifnotnull(peer->fakename);
	freeifnotnull(peer->fakegroup);
	if (peer->ratioinfo) ratio_finish(peer->ratioinfo);
	freeifnotnull(peer->supgids);
}

char *mkconfrealstr(int sectionid, char *item, char *defaul)
{
	char *setting;
	
	loadstrfromconfig(config->configfile, sectionid, item, &(setting), defaul);
	if (setting == NULL)
		return(NULL);
	
	setting = strdupwrapper(setting);
	
	converttorealstr(setting);
	return(setting);
		
	return(setting);
}

char *mktokconfstr(TOKENSET *tset, int sectionid, char *item, char *defaul)
{
	char *setting;
	
	loadstrfromconfig(config->configfile, sectionid, item, &(setting), defaul);
	if (setting == NULL)
		return(NULL);
	
	setting = strdupwrapper(setting);		
	setting = tokenset_apply(tset, setting, FALSE);
	return(setting);
}

int mktokconfint(TOKENSET *tset, int sectionid, char *item, char *format,
	         char *defaultstr, int defaultint)
{
	char *setting;
	int ret;
	
	loadstrfromconfig(config->configfile, sectionid, item, &(setting), defaultstr);
	if (setting == NULL)
		return(defaultint);
	setting = strdupwrapper(setting);
	setting = tokenset_apply(tset, setting, FALSE);
	if (sscanf(setting, format, &ret) != 1)
		ret = defaultint;
	freewrapper(setting);
	return(ret);
}

void buildtokset(TOKENSET *tset, PERMSTRUCT *s, void *a)
{
	gid_t *list;
	uid_t inuid;
	gid_t ingid;
	int result;
	char *str;
	
	if (s->getuseruid)
	{
		inuid = s->getuseruid(a);
		result = (int)inuid;
		str = safe_snprintf("%d", result);
		tokenset_settoken(tset, 'u', str);
	}
	
	if (s->getusergid)
	{
		ingid = s->getusergid(a);
		result = (int)ingid;
		str = safe_snprintf("%d", result);
		tokenset_settoken(tset, 'g', str);
	}
	
	if (s->gethomedir)
		tokenset_settoken(tset, 'h', strdupwrapper(s->gethomedir(a)));
	if (s->getrootdir)
		tokenset_settoken(tset, 'r', strdupwrapper(s->getrootdir(a)));
	if (s->getusersupgid)
	{
		list = s->getusersupgid(a);
		tokenset_settoken(tset, 'G', makegidliststr(list));
		freewrapper(list);
	}
}	

void setupacls(FTPSTATE *peer, TOKENSET *tset, int section, char *funcname, int aclfunc)
{
	char *data;
	int occur = 1;
	int escapetoks = (aclfunc != 0);
	
	while((data = getconfigdata(config->configfile, section, funcname, occur)))
	{
		char *pos;

		data = tokenset_apply(tset, strdupwrapper(data), escapetoks);
		if ((pos = strrchr(data, ':')) != NULL)
		{
			/* this will temporaryly damage config data. But
			   I fix it afterwards :) */
			*pos = 0;
			acllist_add(peer->acldata, data, pos + 1, aclfunc);
		}
		
		freewrapper(data);
		
		occur++;
	}
}

int transferconfig(FTPSTATE *peer, TOKENSET *tset, int section)
{
	int occur, result = TRUE;
	char *data;
	int intdata;
	
	peer->timeout = mktokconfint(tset, section, "timeout", "%d", NULL, peer->vserver->timeout);
	peer->maxtimeout = peer->timeout;

	/* get uid setting */
	loadstrfromconfig(config->configfile, section, "uid", &(data), "%u");
	data = strdupwrapper(data);
	data = tokenset_apply(tset, data, FALSE);
	if (sscanf(data, "%d", &intdata) != 1)
	{
		/* try for username then */
		struct passwd *pwdent;
		pwdent = getpwnam(data);
		if (pwdent)
			peer->uidt_asuid = pwdent->pw_uid;
		else
			peer->uidt_asuid = config->uidt_nobodyuid;
	}
	else
	{
		peer->uidt_asuid = (uid_t)intdata;
	}
	freewrapper(data);

	/* get gid setting */
	loadstrfromconfig(config->configfile, section, "gid", &(data), "%g");
	data = strdupwrapper(data);
	data = tokenset_apply(tset, data, FALSE);
	
	if (sscanf(data, "%d", &intdata) != 1)
	{
		/* if it starts with a "!" mark, get gid of username */
		if (data[0] == '!')
		{
			struct passwd *pwdent;
			pwdent = getpwnam(data+1);
			if (pwdent)
				peer->gidt_asgid = pwdent->pw_gid;
			else
				peer->gidt_asgid = config->gidt_nobodygid;
		}
		else	
		{
			/* try for groupname then */
			struct group *grent;
			grent = getgrnam(data);
			if (grent)
				peer->gidt_asgid = grent->gr_gid;
			else
				peer->gidt_asgid = config->gidt_nobodygid;
		}
	}
	else
	{
		peer->gidt_asgid = (gid_t)intdata;
	}
	
	freewrapper(data);

	if ((int)peer->uidt_asuid == 0)
		peer->uidt_asuid = config->uidt_nobodyuid;
	if ((int)peer->gidt_asgid == 0)
		peer->gidt_asgid = config->gidt_nobodygid;

	peer->homedir = mktokconfstr(tset, section, "homedir", "%h");
	if (!checkabsdir(peer->homedir))
		result = authlogmsg("homedir", peer->groupname);

	peer->basedir = mktokconfstr(tset, section, "rootdir", "%r");
	if (!checkabsdir(peer->basedir))
		result = authlogmsg("rootdir", peer->groupname);


	peer->umask = mktokconfint(tset, section, "umask", "%o", NULL, peer->vserver->umask) & 0777;
	peer->chmodable = mktokconfint(tset, section, "chmoding", "%d", NULL, 0);
	peer->jailenabled = mktokconfint(tset, section, "userjail", "%d", NULL, 0);
	peer->maxusers = mktokconfint(tset, section, "maxusers", "%d", NULL, config->defaults->maxusers);
	peer->maxtranspd = mktokconfint(tset, section, "maxspeed", "%d", NULL, 0);
	peer->maxtranspd_down = mktokconfint(tset, section, "maxspeeddown", "%d", NULL, 0);
	peer->maxtranspd_up = mktokconfint(tset, section, "maxspeedup", "%d", NULL, 0);
	peer->chroot = mktokconfint(tset, section, "chroot", "%d", NULL, FALSE);
	peer->droproot = mktokconfint(tset, section, "droproot", "%d", NULL, FALSE);

	peer->nicevalue = mktokconfint(tset, section, "nice", "%d", NULL, 0);
	peer->accessdevices = mktokconfint(tset, section, "devaccess", "%d", NULL, FALSE);
	peer->realdir = mktokconfint(tset, section, "realdir", "%d", NULL, FALSE);
	peer->fxpallow = mktokconfint(tset, section, "fxpallow", "%d", NULL, FALSE);
	
	peer->fakemode = mktokconfint(tset, section, "fakemode", "%o", NULL, -1);
	peer->fakename = mktokconfstr(tset, section, "fakename", NULL);
	peer->fakegroup = mktokconfstr(tset, section, "fakegroup", NULL);

	peer->cwddumpdata = mkconfrealstr(section, "cddumpdata", NULL);
	peer->logindumpdata = mkconfrealstr(section, "welcomedumpdata", NULL);
	peer->quitdumpdata = mkconfrealstr(section, "quitdumpdata", NULL);
	peer->busydumpdata = mkconfrealstr(section, "busydumpdata", NULL);

	if (!peer->cwddumpdata)
		peer->cwddump = mktokconfstr(tset, section, "cddump", NULL);

	if (!peer->logindumpdata)
	{
		peer->logindump = mktokconfstr(tset, section, "welcome", NULL);
		if (!checkabsdir(peer->logindump))
			result = authlogmsg("welcome", peer->groupname);
	}
	
	if (!peer->quitdumpdata)
	{
		peer->quitdump = mktokconfstr(tset, section, "quitdump", NULL);
		if (!checkabsdir(peer->quitdump))
			result = authlogmsg("quitdump", peer->groupname);
	}
	
	if (!peer->busydumpdata)
	{
		peer->busydump = mktokconfstr(tset, section, "busydump", NULL);
		if (!checkabsdir(peer->busydump))
			result = authlogmsg("busydump", peer->groupname);
	}

	peer->acldata = acllist_create();

	setupacls(peer, tset, section, "fnaccess", 1);
	setupacls(peer, tset, section, "pfnaccess", 2);
	setupacls(peer, tset, section, "access", 0);
	
	occur = 1;
	while((data = getconfigdata(config->configfile, section, "cmdoff", occur++)))
		disableset_disablecmd(peer->cmddisableset, mainftpcmd, data);
	occur = 1;
	while((data = getconfigdata(config->configfile, section, "sitecmdoff", occur++)))
		disableset_disablecmd(peer->sitedisableset, siteftpcmd, data);

	if (mktokconfint(tset, section, "ratios", "%d", NULL, FALSE))
		peer->ratioinfo = ratio_loaduser(peer->username, section);
	else
		peer->ratioinfo = NULL;

	data = mktokconfstr(tset, section, "supgid", "%G");
	peer->supgids = parsegidlist(data);
	freewrapper(data);
	
	return(result);
}

int auth_getcursectionid(FTPSTATE *peer)
{
	return(getsectionid(config->configfile, peer->groupname));
}

void *auth_getconfigcache(void)
{
	return(config->configfile);
}

char *auth_getremotename(FTPSTATE *peer)
{
	return(peer->hostname);
}

unsigned int *auth_getremoteip(FTPSTATE *peer)
{
	return(&(peer->remoteip));
}


PERMSTRUCT *configauthmethod(char *group, char *authmethod)
{
	int count = 0;
	static PERMSTRUCT dp;

#ifdef HAVE_DLOPEN
	if (authmethod[0] == '/')
	{
		/* assume it is a libary module */
#ifndef RTLD_NOW
		dp.handle = dlopen(authmethod, 1);
#else
		dp.handle = dlopen(authmethod, RTLD_NOW);
#endif
		if (dp.handle == NULL)
		{
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("could not open or link '%s', reason: %s", authmethod, dlerror()));
			return(NULL);
		}
		dp.gethandle = dlsym(dp.handle, "gethandle");
		dp.freehandle = dlsym(dp.handle, "freehandle");
		dp.chkpasswd = dlsym(dp.handle, "chkpasswd");
		dp.gethomedir = dlsym(dp.handle, "gethomedir");
		dp.getrootdir = dlsym(dp.handle, "getrootdir");
		dp.getuseruid = dlsym(dp.handle, "getuseruid");
		dp.getusergid = dlsym(dp.handle, "getusergid");
		dp.getusersupgid = dlsym(dp.handle, "getusersupgid");

		/* make sure the required functions exist and are defined. */
		if (!dp.gethandle || !dp.freehandle || !dp.chkpasswd)
		{
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("linked module '%s' incomplete", authmethod));
			dlclose(dp.handle);
			return(NULL);
		}
		return(&dp);
	}
#endif
	while(authtypes[count].authname != NULL)
	{
		if (strcasecmp(authmethod, authtypes[count].authname) == 0)
			return(authtypes[count].authstruct);
		count++;
	}
	
	log_giveentry(MYLOG_INFO, NULL, safe_snprintf("authmethod '%s' does not exist, used by group %s", authmethod, group));
	return(NULL);
}

/* this finds the section the user belongs to */

char *auth_getgroup(FTPSTATE *peer, TOKENSET *tset, int *section, PERMSTRUCT **rprog, void **rinf)
{
	int count = 0;
	int sectionid, result;
	char *mode, *authuser;
	char **grouplist = peer->vserver->grouplist;
	PERMSTRUCT *am = NULL;
	void *handle = NULL;
	
	while(grouplist[count] != NULL)
	{
		sectionid = getsectionid(config->configfile, grouplist[count]);
		if (sectionid == -1)
		{
			/* stop authenticating if error occurs */
			log_giveentry(MYLOG_INFO, NULL, safe_snprintf("group %s does not exist", grouplist[count]));
			return(NULL);
		}
		else
		{
			authuser = mktokconfstr(tset, sectionid, "authuser", peer->username);
			mode = getconfigdata(config->configfile, sectionid, "authmethod", 1);
			if (mode)
				am = configauthmethod(grouplist[count], mode);
			if (am)
			{
				/* Check ip */
				IPACLLIST *clist;
				
				clist = ipacllist_new(config->configfile, sectionid, "ipacl");
				if (!user_allowed(clist, peer->remoteip, peer->hostname))
					am = NULL;
				ipacllist_destroy(clist);
			}
			else
				return(NULL);
			if (am)
				/* Check username */
				if (!checknamelist(config->configfile, sectionid, peer->username))
					am = NULL;
			if (am)
			{
				/* now see if valid user */
				peer->groupname = grouplist[count];
				handle = am->gethandle(peer, tset, authuser, &result);
				/* if the authentication module returns ERROR, return now! */
				if (result == AUTH_ERROR)
				{
					freewrapper(authuser);
					return(NULL);	
				}
			}
			
			if (handle)
			{
				*section = sectionid;
				*rprog = (void *)am;
				*rinf = handle;
				freewrapper(authuser);
				return(grouplist[count]);
			}
			freewrapper(authuser);
		}
		count++;
	}
	return(NULL);
}

char *setuseropts(FTPSTATE *peer, char *password)
{
	int section;
	void *authhandle = NULL;
	char *errstr = NULL;
	PERMSTRUCT *authcmd;
	TOKENSET *tset = tokenset_new();
	
	/* Check possible exploits here! */ 
	if (checkexploits(peer))
		goto authend;		

	peer->loggedin = FALSE;
	tokenset_settoken(tset, 'U', strdupwrapper(peer->username));
	tokenset_settoken(tset, 'v', strdupwrapper(peer->vserver->sectionname));
	tokenset_settoken(tset, 'V', strdupwrapper(peer->vserver->vhostname));
	
	peer->groupname = auth_getgroup(peer, tset, &section, &authcmd, &authhandle);

	if (peer->groupname == NULL)
		goto authend;
	
	if (!authcmd->chkpasswd(authhandle, password, &errstr))
		goto authend;
	
	clearauth(peer);
	buildtokset(tset, authcmd, authhandle);
	 
	if (!transferconfig(peer, tset, section))
		goto authend;
	
	peer->loggedin = TRUE;

authend:
	tokenset_finish(tset);
	if (authhandle)
	{
		authcmd->freehandle(authhandle);
#ifdef HAVE_DLOPEN
		if (authcmd->handle)
			dlclose(authcmd->handle);
#endif
	}
	
	if (peer->loggedin)
	{
		freeifnotnull(errstr);
		return(NULL);
	}
	else
	{
		if (!errstr)
			errstr = strdupwrapper("Bad password");
		return(errstr);
	}
}
