#include #include #include #include #include #include #include #include #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) \ ? "" : 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 . If * no explict specification for this file, return the * default access list. If 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 the compressed pathname derived from . * 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 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