#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <ctype.h>
#include <arpa/tftp.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include "tftpyale.h"

#include "classes/config.h"
#include "classes/dict.h"
#include "classes/access.h"

/* Enhancements to tftpd made at Yale:
 * 07-Mar-89
 * Alan S. Watt
 * watt-alan@cs.yale.edu
 *
 * Control such things as default directory and debugging level from
 * configuration file (/usr/local/etc/tftpd.conf)
 *
 * Permit access lists to say which systems are allowed access over
 * specific files.
 *
 * 10-Aug-92
 * Alan S. Watt
 * Fixed a number of botches resulting from the merge with the UUNET
 * base source.  Converted to use the Dictionary and AccessGroup classes.
 *
 * 12-Sept-89
 * Alan S. Watt
 * watt-alan@cs.yale.edu
 *
 * Modified the access list maintenance so exact matches (no mask bits)
 * are kept at the head of the list and permission searches end on the
 * first address match.  This has the effect of giving exact matches
 * a higher priority than wildcard matches.  This is also the way the
 * cisco access lists work.
 */

/*  We export only the following globals:
 *
 * char *tftpDefaultDirectory;
 * char *tftpRootDirectory;
 * int tftpDebugLevel;
 * int configLineNumber;
 * int validateAccessToFile(char* filename, unsigned long sourceAddress);
 * int readConfigFile(int argc, char** argv);
 * int realPath(char* orig, char* buf);
 *
 */


/* location of the configuration file */
int	configLineNumber;

/* If set, it means we have chdir() and chroot() to this directory and
 * unqualified pathnames are OK.  Otherwise they are illegal.
 */
char*	tftpDefaultDirectory	= (char*)0;
char*	tftpRootDirectory	= (char*)0;
int	tftpDebugLevel		= 0;
int	maxInputWait		= 5*60;		/* default wait for 5 minutes */

static Dict fileAccessDict;
static AccessGroup accessLists;
static Dict commandDict;

/* declare locals required for access lists
 */
static char*		accessFormatError;
static char*		cfgets();
static struct stat	configStat;
static int		defaultAccessList;

/* for debugging printouts */
static char* permNames[] ={ "deny", "readonly", "writeonly", "readwrite" };
#define NITEMS(X)	(sizeof(X) / sizeof (X)[0])
#define PERMNAME(N)	((unsigned)(N) >= NITEMS(permNames) \
				? "<unknown>" : permNames[N])


#define CMD_DEFAULT_DIR		1
#define CMD_DEBUG_LEVEL		2
#define CMD_ROOT_DIR		3
#define CMD_ACCESS_LIST		4
#define CMD_DEFAULT_ACCESS_LIST	5
#define CMD_RESTRICT		6
#define CMD_INPUT_WAIT		7

/* Add a file restriction clause to the list */
static
addFileRestriction (ac, av)
int	ac;
char**	av;
{
	int list;

	ac--; av++;
	if (ac != 2) {
		accessFormatError = "'restrict' command requires 2 arguments";
		return 0;
	}

	/* get list number */
	list = atoi (av[1]);
	if (list <= 0) {
		accessFormatError = "list argument not positive integer";
		return 0;
	}

	dict_add (fileAccessDict, *av, (void*)list);
	return 0;
}


/* primitive version of cfgets function; doesn't process continuations */
static char*
cfgets (buf, num, file, lineCount)
char*	buf;
int	num;
FILE*	file;
int	*lineCount;
{
	char* ret;

	for (;;) {
		ret = fgets(buf, num, file);
		if (ret == NULL)
			break;
		if (lineCount != (int*)0)
			*lineCount += 1;
		if (buf[0] != '#')
			break;
	}
	return ret;
}



/* Return whether the file named by the argument stat structure
 * is the same as the configuration file.
 */
isConfigFile (sb)
struct stat* sb;
{
	if (sb->st_dev == configStat.st_dev
	    && sb->st_ino == configStat.st_ino)
		return 1;
	return 0;
}

/* Get the access list which covers <fileName>.  If
 * no explict specification for this file, return the
 * default access list.  If <fileName> is fully
 * qualified (starts with '/') check to see if the
 * prefix matches the default directory.
 */
static int
getAccessList (fileName)
char* fileName;
{
	unsigned int list;
	char* rindex();

	if (*fileName == '/') {
		char* sep = rindex (fileName, '/');
		int count = sep - fileName;

		if (count > 0)
			count--;
		if (tftpDefaultDirectory
		    && strncmp(fileName, tftpDefaultDirectory,
				strlen(tftpDefaultDirectory))==0) {
			fileName = sep+1;
		}
	}

	list = (int)dict_find (fileAccessDict, fileName);

	return list ? list : defaultAccessList;
}


/* allocate space for a string and copy it there */
static char*
newString (arg)
char* arg;
{
	char* tmp;
	if (arg == NULL)
		return NULL;

	tmp = malloc(strlen(arg)+1);
	strcpy (tmp, arg);
	return tmp;
}


/* (re)-read the tftpd config file.  We keep an open handle
 * on the file and check to see if it's been modified since
 * the last read.
 */

readConfigFile(argc, argv)
int	argc;
char**	argv;
{
	static FILE* configf;
	static unsigned long configModTime;
	char	buf[256];
	int	cargc;
	char**	cargv;
	char*	fileName;
	struct stat statb;
	Config	cnf;

	/* If given an argument, it must be the name
	 * of a configuration file.  Otherwise use
	 * the default.
	 */
	if (tftpDebugLevel > 3) {
		syslog(LOG_DEBUG, "readConfigFile(%d,0x%08x)", argc, argv);
	}
	if (configf == 0) {
		fileName = dfltConfigFile;
		if ((argc--,argv++), argc > 0) {
			if (access(*argv, 0) == 0)
				fileName = *argv;
		}
		if ((configf = fopen (fileName, "r")) == NULL) {
			syslog (LOG_ERR,
				"Cannot open configFile '%s'; reason = %s",
				fileName, strerror(errno));
			return 0;
		}
	}
	if (tftpDebugLevel > 3) {
		syslog(LOG_DEBUG, "configFile = '%s'; FILE* = 0x%08x",
			fileName, configf);
	}

	/* See if the file has been changed since the last time we
	 * read it.
	 * @@@ This needs to use "stat" instead, in case people
	 * @@@ use editors which replace the file instead of over-write
	 * @@@ it.
	 */
	fstat(fileno(configf), &statb);
	configStat = statb;
	if (statb.st_mtime == configModTime)
		return;
	configModTime = statb.st_mtime;

	/* Read the config file */
	rewind (configf);
	resetConfig();
	configLineNumber = 0;

	cnf = config_new();
	config_setoptions (cnf, config_getoptions (cnf)& ~CFG_OPT_CASEMAP);

	while (cfgets (buf, sizeof buf, configf, &configLineNumber) != NULL) {
		char* end;

		cargc = config_scanbuf (cnf, buf);
		if (cargc == 0)
			continue;

		cargv = config_fields(cnf);
		switch ((int)dict_find (commandDict, cargv[0])) {
		  /* specify default directory */
		  case CMD_DEFAULT_DIR:
			if (cargc != 2)
				break;
			if (chdir(cargv[1])) {
				if (tftpDebugLevel > 0) {
					syslog (LOG_DEBUG,
						"chdir fails; '%s'",
						strerror(errno));
				}
			}
			else
				tftpDefaultDirectory = newString(cargv[1]);
			break;

		  /* specify virtual root directory */
		  case CMD_ROOT_DIR:
			if (cargc == 2) {
				if (access(cargv[1], 0) != 0)
					syslog(LOG_DEBUG, "directory '%s': %s",
						cargv[1], strerror(errno));
				else
					tftpRootDirectory = newString(cargv[1]);
			}
			break;

		  /* set debugging level */
		  case CMD_DEBUG_LEVEL:
			if (cargc != 2)
				break;

			/* error log ?? */
			tftpDebugLevel = atoi(cargv[1]);
			break;

		  /* create or extend an access list */
		  case CMD_ACCESS_LIST:
			accessGroup_add (accessLists, cargc, cargv);
			break;

		  /* specify default access list */
		  case CMD_DEFAULT_ACCESS_LIST:
			if (cargc == 2)
				defaultAccessList = atoi(cargv[1]);
			/* error log ?? */
			break;

		  /* restrict access to a file by access list */
		  case CMD_RESTRICT:
			addFileRestriction(cargc, cargv);
			break;

		  /* set input timeout period */
		  case CMD_INPUT_WAIT:
			{ int n;
			  if (cargc != 2)
				break;
			  n = atoi (cargv[1]);
			  if (n > 0)
				maxInputWait = n;
			}
			break;
			

		  default:
			/* log illegal configuration options */
			syslog (LOG_DEBUG,
			       "Line %d: illegal configuration keyword '%s'",
				configLineNumber, cargv[0]);
			break;
			
		}
	}
	config_dispose (cnf);
	if (tftpDebugLevel > 0) {
		static char logInfo[] =
		   "init_config: default=%s; root=%s; debug=%d";
		
		syslog (LOG_DEBUG, logInfo,
			(tftpDefaultDirectory ? tftpDefaultDirectory : ""),
			(tftpRootDirectory ? tftpRootDirectory : ""),
			tftpDebugLevel);
	}
	if (tftpDebugLevel > 4) {
		accessDebugDump();
	}

	return 1;
}

/* realPath(char* orig, char* buf)
 * Deposit into <buf> the compressed pathname derived from <orig>.
 * This deletes any "." and ".." components, as well as redundant
 * separator characters.
 *
 * The re-created path always starts with a '/'.
 *
 * UNIX-specific.
 */
#define MAXCOMPONENT	256
#define MAXPATH		1024
#define SEPCHAR		'/'
#define ISCURDIR(S)	(strcmp(S,thisDir)==0)
#define ISPARENTDIR(S)	(strcmp(S,parentDir)==0)

static char	thisDir[]	= ".";
static char	parentDir[]	= "..";
static char	sepDir[]	= "/";
static char	rootDir[]	= "/";

int
realPath(orig, buf)
char*	orig;
char*	buf;
{
	char*	obuf[MAXCOMPONENT];
	char*	nbuf[MAXCOMPONENT];
	char	tmp[MAXPATH];
	int	numComponents;
	int	pathSize;
	int	num;

	/* Count the maximum number of components which could be
	 * in the source path.
	 */
	{
		register char* p;

		numComponents = 1;
		for (p = orig; *p != '\0'; p++)
			if (*p == SEPCHAR)
				numComponents++;
	}

	/* make sure the stack locals have enough space to hold
	 * everything.  If not return failure.
	 */
	if (numComponents >= MAXCOMPONENT)
		return -1;
	pathSize = strlen (orig) + 1;
	if (pathSize >= MAXPATH)
		return -1;

	/* copy in path to temporary */
	strcpy (tmp, orig);

	/* dissect it into components */
	{
		register char* p, *q;

		num = 0;
		for (p = q = tmp; p != 0; p = q) {

			/* skip any separators */
			while (*p == SEPCHAR)
				p++;

			/* if at end of string quit */
			if  (*p == '\0')
				break;

			/* store this component away */
			obuf[num++] = p;

			/* get end of this component and replace
			 * with string terminator
			 */
			q = strchr (p, SEPCHAR);
			if (q != 0)
				*q++ = '\0';
			
		}
	}

	/* squeeze out current and parent entries */
	{
		register i, j;

		for (i=0, j=0; i < num; i++) {
			char* cur = obuf[i];
			if (ISCURDIR(cur))
				continue;
			else if (ISPARENTDIR(cur)) {
				if (j > 0)
					j--;
			}
			else
				nbuf[j++] = cur;
		}
		nbuf[j] = 0;
		num = j;
	}

	/* re-create copy of path */
	buf[0] = '\0';
	if (num == 0)
		strcpy (buf, rootDir);
	else {
		register int i;
		for (i = 0; i < num; i++) {
			strcat (buf, sepDir);
			strcat (buf, nbuf[i]);
		}
	}

	return 0;
}


static struct CMDS {
	char* cmdName;
	int cmdVal;
} configCmds[] ={
	"default-directory",	CMD_DEFAULT_DIR,
	"defaultDirectory",	CMD_DEFAULT_DIR,
	"debug-level",		CMD_DEBUG_LEVEL,
	"debugLevel",		CMD_DEBUG_LEVEL,
	"root-directory",	CMD_ROOT_DIR,
	"rootDirectory",	CMD_ROOT_DIR,
	"access-list",		CMD_ACCESS_LIST,
	"accessList",		CMD_ACCESS_LIST,
	"default-access-list",	CMD_DEFAULT_ACCESS_LIST,
	"defaultAccessList",	CMD_DEFAULT_ACCESS_LIST,
	"restrict",		CMD_RESTRICT,
	"inputWait",		CMD_INPUT_WAIT,
	"input-wait",		CMD_INPUT_WAIT,
	(char*)0,		0
};

/* reset configuration options to defaults */
resetConfig()
{
	register int i;
	extern char* dfltDefaultDirectory;
	extern char* dfltRootDirectory;
	extern int dfltDebugLevel;
	extern char* dfltConfigFile;

	extern Dict fileAccessDict;
	extern AccessGroup accessLists;
	extern Dict commandDict;


	dict_dispose (fileAccessDict);
	fileAccessDict = dict_new();

	accessGroup_dispose (accessLists);
	accessLists = accessGroup_new();

	if (commandDict == 0) {
		register struct CMDS* cp;
		commandDict = dict_new();
		for (cp = configCmds; cp->cmdName != 0 ; cp++)
			dict_add (commandDict, cp->cmdName, (void*)cp->cmdVal);
		
	}


	/* reset debug level */
	tftpDebugLevel = dfltDebugLevel;

	/* reset default directory */
	if (tftpDefaultDirectory) {
		free (tftpDefaultDirectory);
		tftpDefaultDirectory = 0;
		if (dfltDefaultDirectory)
			tftpDefaultDirectory = newString(dfltDefaultDirectory);
	}

	/* reset root directory */
	if (tftpRootDirectory) {
		free (tftpRootDirectory);
		tftpRootDirectory = 0;
		if (dfltRootDirectory)
			tftpRootDirectory = newString(dfltRootDirectory);
	}
}


/* validate that the source system named by:
 * 'source' is permitted to access the
 * given filename according to access lists.
 *
 * Return 1 if access is permitted under old default rules
 * (i.e., no access list governs); 0 if not.
 *
 * Return 2 if more particular access is permitted by an access list.
 * This gets around the old public read/writable requirements.
 */ 
int
validateAccessToFile (filename, source, type)
char* filename;
unsigned long source; 
int type;
{
	int list;
	int ret;

	list = getAccessList (filename);

	if (list == 0)
		return 1;

	/* If the named list doesn't exist, use the default */
	ret = accessGroup_validateAddress (accessLists, list, source, 3);
	if (ret < 0) {
		if (list == defaultAccessList
			|| ((list = defaultAccessList) == 0))
			return 1;
	}

	/* If that doesn't exist, return OK */
	ret = accessGroup_validateAddress (accessLists, list, source, 3);
	if (ret < 0)
		return 1;

	if (tftpDebugLevel > 1) {
		struct in_addr t;
		t.s_addr = source;
		syslog(LOG_DEBUG, "access to '%s' for %s is %s in list %d",
			filename, inet_ntoa(t), PERMNAME(ret), list);
	}

	if (type == RRQ)
		return (ret & 1) ? 2 : 0;
	return (ret & 2) ? 2 : 0;
}


/* Debugging functions */


#ifndef DEBUG
accessDebugDump()
{
}
#else
#include <stdio.h>

accessDebugDump()
{
	FILE*	logf;
	char**	keys;
	int	val;
	int	i;

	logf = fopen ("tftpdebug.log", "a");
	if (logf == 0)
		return;

	fprintf (logf, "After reading config file:\n");
	fprintf (logf, "File Restriction list:\n");
	dict_printOn (fileAccessDict, logf);
	keys = dict_keys (fileAccessDict);
	for (i = 0; keys[i] != 0; i++)
		fprintf (logf, "\trestrict(%s) = %d\n",
			 keys[i], (int)dict_find (fileAccessDict, keys[i]));
	free ((char*)keys);
	fprintf (logf, "Access lists:\n");
	accessGroup_printOn (accessLists, logf);
	fprintf (logf, "\n\n");

	fclose (logf);
}

#endif


syntax highlighted by Code2HTML, v. 0.9.1