From f6f7bfb8e0a257137f8c2dad83ae4ed826b4e4bb Mon Sep 17 00:00:00 2001 From: Eric Andersen Date: Tue, 22 Oct 2002 12:24:59 +0000 Subject: last_patch63 from vodz: add in crond and crontab applets --- miscutils/Makefile.in | 2 + miscutils/config.in | 2 + miscutils/crond.c | 1146 +++++++++++++++++++++++++++++++++++++++++++++++++ miscutils/crontab.c | 393 +++++++++++++++++ 4 files changed, 1543 insertions(+) create mode 100644 miscutils/crond.c create mode 100644 miscutils/crontab.c (limited to 'miscutils') diff --git a/miscutils/Makefile.in b/miscutils/Makefile.in index 078f87001..a5e12e7ee 100644 --- a/miscutils/Makefile.in +++ b/miscutils/Makefile.in @@ -25,6 +25,8 @@ endif MISCUTILS-y:= MISCUTILS-$(CONFIG_ADJTIMEX) += adjtimex.o +MISCUTILS-$(CONFIG_CROND) += crond.o +MISCUTILS-$(CONFIG_CRONTAB) += crontab.o MISCUTILS-$(CONFIG_DC) += dc.o MISCUTILS-$(CONFIG_DUTMP) += dutmp.o MISCUTILS-$(CONFIG_MAKEDEVS) += makedevs.o diff --git a/miscutils/config.in b/miscutils/config.in index 20310eb6c..1d2751444 100644 --- a/miscutils/config.in +++ b/miscutils/config.in @@ -7,6 +7,8 @@ mainmenu_option next_comment comment 'Miscellaneous Utilities' bool 'adjtimex' CONFIG_ADJTIMEX +bool 'crond' CONFIG_CROND +bool 'crontab' CONFIG_CRONTAB bool 'dc' CONFIG_DC bool 'dutmp' CONFIG_DUTMP bool 'makedevs' CONFIG_MAKEDEVS diff --git a/miscutils/crond.c b/miscutils/crond.c new file mode 100644 index 000000000..225d5026c --- /dev/null +++ b/miscutils/crond.c @@ -0,0 +1,1146 @@ +/* + * crond -d[#] -c -f -b + * + * run as root, but NOT setuid root + * + * Copyright 1994 Matthew Dillon (dillon@apollo.west.oic.com) + * May be distributed under the GNU General Public License + * + * Vladimir Oleynik (C) 2002 to be used in busybox + */ + +#define VERSION "2.3.2" + +#undef FEATURE_DEBUG_OPT + + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "busybox.h" + +#define arysize(ary) (sizeof(ary)/sizeof((ary)[0])) + +#ifndef CRONTABS +#define CRONTABS "/var/spool/cron/crontabs" +#endif +#ifndef TMPDIR +#define TMPDIR "/var/spool/cron" +#endif +#ifndef LOG_FILE +#define LOG_FILE "/var/log/cron" +#endif +#ifndef SENDMAIL +#define SENDMAIL "/usr/sbin/sendmail" +#endif +#ifndef SENDMAIL_ARGS +#define SENDMAIL_ARGS "-t", "-oem", "-i" +#endif +#ifndef CRONUPDATE +#define CRONUPDATE "cron.update" +#endif +#ifndef MAXLINES +#define MAXLINES 256 /* max lines in non-root crontabs */ +#endif + + + +typedef struct CronFile { + struct CronFile *cf_Next; + struct CronLine *cf_LineBase; + char *cf_User; /* username */ + int cf_Ready; /* bool: one or more jobs ready */ + int cf_Running; /* bool: one or more jobs running */ + int cf_Deleted; /* marked for deletion, ignore */ +} CronFile; + +typedef struct CronLine { + struct CronLine *cl_Next; + char *cl_Shell; /* shell command */ + int cl_Pid; /* running pid, 0, or armed (-1) */ + int cl_MailFlag; /* running pid is for mail */ + int cl_MailPos; /* 'empty file' size */ + char cl_Mins[60]; /* 0-59 */ + char cl_Hrs[24]; /* 0-23 */ + char cl_Days[32]; /* 1-31 */ + char cl_Mons[12]; /* 0-11 */ + char cl_Dow[7]; /* 0-6, beginning sunday */ +} CronLine; + +#define RUN_RANOUT 1 +#define RUN_RUNNING 2 +#define RUN_FAILED 3 + +#define DaemonUid 0 + +#ifdef FEATURE_DEBUG_OPT +static short DebugOpt; +#endif + +static short LogLevel = 8; +static short ForegroundOpt; +static short LoggerOpt; +static const char *LogFile = LOG_FILE; +static const char *CDir = CRONTABS; + +static void log(int level, const char *ctl, ...); +static void log9(const char *ctl, ...); +static void startlogger(void); + +static void CheckUpdates(void); +static void SynchronizeDir(void); +static int TestJobs(time_t t1, time_t t2); +static void RunJobs(void); +static int CheckJobs(void); +static void RunJob(CronFile *file, CronLine *line); +static void EndJob(const CronFile *file, CronLine *line); + +static void DeleteFile(const char *userName); + +static CronFile *FileBase; + + +int +crond_main(int ac, char **av) +{ + int i; + + opterr = 0; /* disable getopt 'errors' message.*/ + + while ((i = getopt(ac,av, +#ifdef FEATURE_DEBUG_OPT + "d:" +#endif + "l:L:fbSc:")) != EOF){ + + switch (i){ + case 'l': + LogLevel = atoi(optarg); + break; +#ifdef FEATURE_DEBUG_OPT + case 'd': + DebugOpt = atoi(optarg); + LogLevel = 0; + break; +#endif + case 'f': + ForegroundOpt = 1; + break; + case 'b': + ForegroundOpt = 0; + break; + case 'S': /* select logging to syslog */ + LoggerOpt = 0; + break; + case 'L': /* select internal file logger */ + LoggerOpt = 1; + if (*optarg != 0) LogFile = optarg; + break; + case 'c': + if (*optarg != 0) CDir = optarg; + break; + default: /* parse error */ + show_usage(); + } + } + + /* + * change directory + */ + + if (chdir(CDir) != 0) + perror_msg_and_die("chdir"); + + /* + * close stdin and stdout, stderr. + * close unused descriptors - don't need. + * optional detach from controlling terminal + */ + + if (ForegroundOpt == 0) { + if(daemon(1, 0) < 0) + perror_msg_and_die("daemon"); + } + + (void)startlogger(); /* need if syslog mode selected */ + signal(SIGHUP,SIG_IGN); /* hmm.. but, if kill -HUP original + * version - his died. ;( + */ + + /* + * main loop - synchronize to 1 second after the minute, minimum sleep + * of 1 second. + */ + + log(9,"%s " VERSION " dillon, started, log level %d\n", av[0], LogLevel); + + SynchronizeDir(); + + { + time_t t1 = time(NULL); + time_t t2; + long dt; + short rescan = 60; + short sleep_time = 60; + + for (;;) { + sleep((sleep_time + 1) - (short)(time(NULL) % sleep_time)); + + t2 = time(NULL); + dt = t2 - t1; + + /* + * The file 'cron.update' is checked to determine new cron + * jobs. The directory is rescanned once an hour to deal + * with any screwups. + * + * check for disparity. Disparities over an hour either way + * result in resynchronization. A reverse-indexed disparity + * less then an hour causes us to effectively sleep until we + * match the original time (i.e. no re-execution of jobs that + * have just been run). A forward-indexed disparity less then + * an hour causes intermediate jobs to be run, but only once + * in the worst case. + * + * when running jobs, the inequality used is greater but not + * equal to t1, and less then or equal to t2. + */ + + if (--rescan == 0) { + rescan = 60; + SynchronizeDir(); + } + CheckUpdates(); +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) + log(5, "Wakeup dt=%d\n", dt); +#endif + if (dt < -60*60 || dt > 60*60) { + t1 = t2; + log9("time disparity of %d minutes detected\n", dt / 60); + } else if (dt > 0) { + TestJobs(t1, t2); + RunJobs(); + sleep(5); + if (CheckJobs() > 0) + sleep_time = 10; + else + sleep_time = 60; + t1 = t2; + } + } + } + /* not reached */ +} + + +static void +vlog(int level, int MLOG_LEVEL, const char *ctl, va_list va) +{ + char buf[1024]; + int logfd; + + if (level >= LogLevel) { + + vsnprintf(buf,sizeof(buf), ctl, va); +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) fprintf(stderr,"%s",buf); + else +#endif + if (LoggerOpt == 0) syslog(MLOG_LEVEL, "%s", buf); + else { + if ((logfd = open(LogFile,O_WRONLY|O_CREAT|O_APPEND,600)) >= 0){ + write(logfd, buf, strlen(buf)); + close(logfd); + } else +#ifdef FEATURE_DEBUG_OPT + perror_msg("Can't open log file") +#endif + ; + } + } +} + +/* + set log_level=9 and log messages +*/ + +static void +log9(const char *ctl, ...) +{ + va_list va; + + va_start(va, ctl); + vlog(9, LOG_WARNING, ctl, va); + va_end(va); +} + +/* + normal logger call point. +*/ + +static void +log(int level, const char *ctl, ...) +{ + va_list va; + + va_start(va, ctl); + vlog(level, LOG_NOTICE, ctl, va); + va_end(va); +} + +/* + Original: void + logfd(int fd, const char *ctl, ...) + Updated to: log_error (used by jobs.c) +*/ + +static void +log_err(const char *ctl, ...) +{ + va_list va; + + va_start(va, ctl); + vlog(20, LOG_ERR, ctl, va); + va_end(va); +} + +/* + used by jobs.c (write to temp file..) +*/ + +static void +fdprintf(int fd, const char *ctl, ...) +{ + va_list va; + + va_start(va, ctl); + vdprintf(fd, ctl, va); + va_end(va); +} + + +static int +ChangeUser(const char *user, short dochdir) +{ + struct passwd *pas; + + /* + * Obtain password entry and change privilages + */ + + if ((pas = getpwnam(user)) == 0) { + log(9, "failed to get uid for %s", user); + return(-1); + } + setenv("USER", pas->pw_name, 1); + setenv("HOME", pas->pw_dir, 1); + setenv("SHELL", "/bin/sh", 1); + + /* + * Change running state to the user in question + */ + + if (initgroups(user, pas->pw_gid) < 0) { + log(9, "initgroups failed: %s %m", user); + return(-1); + } + if (setregid(pas->pw_gid, pas->pw_gid) < 0) { + log(9, "setregid failed: %s %d", user, pas->pw_gid); + return(-1); + } + if (setreuid(pas->pw_uid, pas->pw_uid) < 0) { + log(9, "setreuid failed: %s %d", user, pas->pw_uid); + return(-1); + } + if (dochdir) { + if (chdir(pas->pw_dir) < 0) { + log(8, "chdir failed: %s %s", user, pas->pw_dir); + if (chdir(TMPDIR) < 0) { + log(9, "chdir failed: %s %s", TMPDIR, user); + return(-1); + } + } + } + return(pas->pw_uid); +} + +static void +startlogger(void) +{ + int logfd; + + if (LoggerOpt == 0) + openlog(applet_name, LOG_CONS|LOG_PID,LOG_CRON); + + else { /* test logfile */ + if ((logfd = open(LogFile,O_WRONLY|O_CREAT|O_APPEND,600)) >= 0) + close(logfd); + else +#ifdef FEATURE_DEBUG_OPT + printf("Failed to open log file '%s' reason: %m", LogFile) +#endif + ; + } +} + + +static const char * const DowAry[] = { + "sun", + "mon", + "tue", + "wed", + "thu", + "fri", + "sat", + + "Sun", + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + NULL +}; + +static const char * const MonAry[] = { + "jan", + "feb", + "mar", + "apr", + "may", + "jun", + "jul", + "aug", + "sep", + "oct", + "nov", + "dec", + + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + NULL +}; + +static char * +ParseField(char *user, char *ary, int modvalue, int off, + const char * const *names, char *ptr) +{ + char *base = ptr; + int n1 = -1; + int n2 = -1; + + if (base == NULL) + return(NULL); + + while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') { + int skip = 0; + + /* + * Handle numeric digit or symbol or '*' + */ + + if (*ptr == '*') { + n1 = 0; /* everything will be filled */ + n2 = modvalue - 1; + skip = 1; + ++ptr; + } else if (*ptr >= '0' && *ptr <= '9') { + if (n1 < 0) + n1 = strtol(ptr, &ptr, 10) + off; + else + n2 = strtol(ptr, &ptr, 10) + off; + skip = 1; + } else if (names) { + int i; + + for (i = 0; names[i]; ++i) { + if (strncmp(ptr, names[i], strlen(names[i])) == 0) { + break; + } + } + if (names[i]) { + ptr += strlen(names[i]); + if (n1 < 0) + n1 = i; + else + n2 = i; + skip = 1; + } + } + + /* + * handle optional range '-' + */ + + if (skip == 0) { + log9("failed user %s parsing %s\n", user, base); + return(NULL); + } + if (*ptr == '-' && n2 < 0) { + ++ptr; + continue; + } + + /* + * collapse single-value ranges, handle skipmark, and fill + * in the character array appropriately. + */ + + if (n2 < 0) + n2 = n1; + + if (*ptr == '/') + skip = strtol(ptr + 1, &ptr, 10); + + /* + * fill array, using a failsafe is the easiest way to prevent + * an endless loop + */ + + { + int s0 = 1; + int failsafe = 1024; + + --n1; + do { + n1 = (n1 + 1) % modvalue; + + if (--s0 == 0) { + ary[n1 % modvalue] = 1; + s0 = skip; + } + } while (n1 != n2 && --failsafe); + + if (failsafe == 0) { + log9("failed user %s parsing %s\n", user, base); + return(NULL); + } + } + if (*ptr != ',') + break; + ++ptr; + n1 = -1; + n2 = -1; + } + + if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') { + log9("failed user %s parsing %s\n", user, base); + return(NULL); + } + + while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n') + ++ptr; + +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) { + int i; + + for (i = 0; i < modvalue; ++i) + log(5, "%d", ary[i]); + log(5, "\n"); + } +#endif + + return(ptr); +} + +static void +FixDayDow(CronLine *line) +{ + short i; + short weekUsed = 0; + short daysUsed = 0; + + for (i = 0; i < arysize(line->cl_Dow); ++i) { + if (line->cl_Dow[i] == 0) { + weekUsed = 1; + break; + } + } + for (i = 0; i < arysize(line->cl_Days); ++i) { + if (line->cl_Days[i] == 0) { + daysUsed = 1; + break; + } + } + if (weekUsed && !daysUsed) { + memset(line->cl_Days, 0, sizeof(line->cl_Days)); + } + if (daysUsed && !weekUsed) { + memset(line->cl_Dow, 0, sizeof(line->cl_Dow)); + } +} + + + +static void +SynchronizeFile(const char *fileName) +{ + int maxEntries = MAXLINES; + int maxLines; + char buf[1024]; + + if (strcmp(fileName, "root") == 0) + maxEntries = 65535; + maxLines = maxEntries * 10; + + if (fileName) { + FILE *fi; + + DeleteFile(fileName); + + if ((fi = fopen(fileName, "r")) != NULL) { + struct stat sbuf; + + if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) { + CronFile *file = calloc(1, sizeof(CronFile)); + CronLine **pline; + + file->cf_User = strdup(fileName); + pline = &file->cf_LineBase; + + while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) { + CronLine line; + char *ptr; + + if (buf[0]) + buf[strlen(buf)-1] = 0; + + if (buf[0] == 0 || buf[0] == '#' || buf[0] == ' ' || buf[0] == '\t') + continue; + + if (--maxEntries == 0) + break; + + memset(&line, 0, sizeof(line)); + +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) + log9("User %s Entry %s\n", fileName, buf); +#endif + + /* + * parse date ranges + */ + + ptr = ParseField(file->cf_User, line.cl_Mins, 60, 0, NULL, buf); + ptr = ParseField(file->cf_User, line.cl_Hrs, 24, 0, NULL, ptr); + ptr = ParseField(file->cf_User, line.cl_Days, 32, 0, NULL, ptr); + ptr = ParseField(file->cf_User, line.cl_Mons, 12, -1, MonAry, ptr); + ptr = ParseField(file->cf_User, line.cl_Dow, 7, 0, DowAry, ptr); + + /* + * check failure + */ + + if (ptr == NULL) + continue; + + /* + * fix days and dow - if one is not * and the other + * is *, the other is set to 0, and vise-versa + */ + + FixDayDow(&line); + + *pline = calloc(1, sizeof(CronLine)); + **pline = line; + + /* + * copy command + */ + + (*pline)->cl_Shell = strdup(ptr); + +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) { + log9(" Command %s\n", ptr); + } +#endif + + pline = &((*pline)->cl_Next); + } + *pline = NULL; + + file->cf_Next = FileBase; + FileBase = file; + + if (maxLines == 0 || maxEntries == 0) + log9("Maximum number of lines reached for user %s\n", fileName); + } + fclose(fi); + } + } +} + +static void +CheckUpdates(void) +{ + FILE *fi; + char buf[256]; + + if ((fi = fopen(CRONUPDATE, "r")) != NULL) { + remove(CRONUPDATE); + while (fgets(buf, sizeof(buf), fi) != NULL) { + SynchronizeFile(strtok(buf, " \t\r\n")); + } + fclose(fi); + } +} + +static void +SynchronizeDir(void) +{ + /* + * Attempt to delete the database. Note that we have to make a copy + * of the string + */ + + for (;;) { + CronFile *file; + char *user; + + for (file = FileBase; file && file->cf_Deleted; file = file->cf_Next) + ; + if (file == NULL) + break; + user = strdup(file->cf_User); + DeleteFile(user); + free(user); + } + + /* + * Remove cron update file + * + * Re-chdir, in case directory was renamed & deleted, or otherwise + * screwed up. + * + * scan directory and add associated users + */ + + remove(CRONUPDATE); + if (chdir(CDir) < 0) { + log9("unable to find %s\n", CDir); + exit(20); + } + { + DIR *dir; + struct dirent *den; + + if ((dir = opendir("."))) { + while ((den = readdir(dir))) { + if (strchr(den->d_name, '.') != NULL) + continue; + if (getpwnam(den->d_name)) + SynchronizeFile(den->d_name); + else + log(7, "ignoring %s\n", den->d_name); + } + closedir(dir); + } else { + log9("Unable to open current dir!\n"); + exit(20); + } + } +} + + +/* + * DeleteFile() - delete user database + * + * Note: multiple entries for same user may exist if we were unable to + * completely delete a database due to running processes. + */ + +static void +DeleteFile(const char *userName) +{ + CronFile **pfile = &FileBase; + CronFile *file; + + while ((file = *pfile) != NULL) { + if (strcmp(userName, file->cf_User) == 0) { + CronLine **pline = &file->cf_LineBase; + CronLine *line; + + file->cf_Running = 0; + file->cf_Deleted = 1; + + while ((line = *pline) != NULL) { + if (line->cl_Pid > 0) { + file->cf_Running = 1; + pline = &line->cl_Next; + } else { + *pline = line->cl_Next; + free(line->cl_Shell); + free(line); + } + } + if (file->cf_Running == 0) { + *pfile = file->cf_Next; + free(file->cf_User); + free(file); + } else { + pfile = &file->cf_Next; + } + } else { + pfile = &file->cf_Next; + } + } +} + +/* + * TestJobs() + * + * determine which jobs need to be run. Under normal conditions, the + * period is about a minute (one scan). Worst case it will be one + * hour (60 scans). + */ + +static int +TestJobs(time_t t1, time_t t2) +{ + short nJobs = 0; + time_t t; + + /* + * Find jobs > t1 and <= t2 + */ + + for (t = t1 - t1 % 60; t <= t2; t += 60) { + if (t > t1) { + struct tm *tp = localtime(&t); + CronFile *file; + CronLine *line; + + for (file = FileBase; file; file = file->cf_Next) { +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) + log(5, "FILE %s:\n", file->cf_User); +#endif + if (file->cf_Deleted) + continue; + for (line = file->cf_LineBase; line; line = line->cl_Next) { +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) + log(5, " LINE %s\n", line->cl_Shell); +#endif + if (line->cl_Mins[tp->tm_min] && + line->cl_Hrs[tp->tm_hour] && + (line->cl_Days[tp->tm_mday] || line->cl_Dow[tp->tm_wday]) && + line->cl_Mons[tp->tm_mon] + ) { +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) + log(5, " JobToDo: %d %s\n", line->cl_Pid, line->cl_Shell); +#endif + if (line->cl_Pid > 0) { + log(8, " process already running: %s %s\n", + file->cf_User, + line->cl_Shell + ); + } else if (line->cl_Pid == 0) { + line->cl_Pid = -1; + file->cf_Ready = 1; + ++nJobs; + } + } + } + } + } + } + return(nJobs); +} + +static void +RunJobs(void) +{ + CronFile *file; + CronLine *line; + + for (file = FileBase; file; file = file->cf_Next) { + if (file->cf_Ready) { + file->cf_Ready = 0; + + for (line = file->cf_LineBase; line; line = line->cl_Next) { + if (line->cl_Pid < 0) { + + RunJob(file, line); + + log(8, "USER %s pid %3d cmd %s\n", + file->cf_User, + line->cl_Pid, + line->cl_Shell + ); + if (line->cl_Pid < 0) + file->cf_Ready = 1; + else if (line->cl_Pid > 0) + file->cf_Running = 1; + } + } + } + } +} + +/* + * CheckJobs() - check for job completion + * + * Check for job completion, return number of jobs still running after + * all done. + */ + +static int +CheckJobs(void) +{ + CronFile *file; + CronLine *line; + int nStillRunning = 0; + + for (file = FileBase; file; file = file->cf_Next) { + if (file->cf_Running) { + file->cf_Running = 0; + + for (line = file->cf_LineBase; line; line = line->cl_Next) { + if (line->cl_Pid > 0) { + int status; + int r = wait4(line->cl_Pid, &status, WNOHANG, NULL); + + if (r < 0 || r == line->cl_Pid) { + EndJob(file, line); + if (line->cl_Pid) + file->cf_Running = 1; + } else if (r == 0) { + file->cf_Running = 1; + } + } + } + } + nStillRunning += file->cf_Running; + } + return(nStillRunning); +} + + + +static void +RunJob(CronFile *file, CronLine *line) +{ + char mailFile[128]; + int mailFd; + + line->cl_Pid = 0; + line->cl_MailFlag = 0; + + /* + * open mail file - owner root so nobody can screw with it. + */ + + snprintf(mailFile, sizeof(mailFile), TMPDIR "/cron.%s.%d", + file->cf_User, getpid()); + mailFd = open(mailFile, O_CREAT|O_TRUNC|O_WRONLY|O_EXCL|O_APPEND, 0600); + + if (mailFd >= 0) { + line->cl_MailFlag = 1; + fdprintf(mailFd, "To: %s\nSubject: cron: %s\n\n", + file->cf_User, + line->cl_Shell + ); + line->cl_MailPos = lseek(mailFd, 0, 1); + } + + /* + * Fork as the user in question and run program + */ + + if ((line->cl_Pid = fork()) == 0) { + /* + * CHILD, FORK OK + */ + + /* + * Change running state to the user in question + */ + + if (ChangeUser(file->cf_User, 1) < 0) + return; + +#ifdef FEATURE_DEBUG_OPT + if (DebugOpt) + log(5, "Child Running %s\n", line->cl_Shell); +#endif + + /* + * stdin is already /dev/null, setup stdout and stderr + */ + + if (mailFd >= 0) { + dup2(mailFd, 1); + dup2(mailFd, 2); + close(mailFd); + } else { + log_err("unable to create mail file user %s file %s, output to /dev/null\n", + file->cf_User, + mailFile + ); + } + execl("/bin/sh", "/bin/sh", "-c", line->cl_Shell, NULL, NULL); + log_err("unable to exec, user %s cmd /bin/sh -c %s\n", + file->cf_User, + line->cl_Shell + ); + fdprintf(1, "Exec failed: /bin/sh -c %s\n", line->cl_Shell); + exit(0); + } else if (line->cl_Pid < 0) { + /* + * PARENT, FORK FAILED + */ + log_err("couldn't fork, user %s\n", file->cf_User); + line->cl_Pid = 0; + remove(mailFile); + } else { + /* + * PARENT, FORK SUCCESS + * + * rename mail-file based on pid of process + */ + char mailFile2[128]; + + snprintf(mailFile2, sizeof(mailFile2), TMPDIR "/cron.%s.%d", + file->cf_User, line->cl_Pid); + rename(mailFile, mailFile2); + } + + /* + * Close the mail file descriptor.. we can't just leave it open in + * a structure, closing it later, because we might run out of descriptors + */ + + if (mailFd >= 0) + close(mailFd); +} + +/* + * EndJob - called when job terminates and when mail terminates + */ + +static void +EndJob(const CronFile *file, CronLine *line) +{ + int mailFd; + char mailFile[128]; + struct stat sbuf; + + /* + * No job + */ + + if (line->cl_Pid <= 0) { + line->cl_Pid = 0; + return; + } + + /* + * End of job and no mail file + * End of sendmail job + */ + + snprintf(mailFile, sizeof(mailFile), TMPDIR "/cron.%s.%d", + file->cf_User, line->cl_Pid); + line->cl_Pid = 0; + + if (line->cl_MailFlag != 1) + return; + + line->cl_MailFlag = 0; + + /* + * End of primary job - check for mail file. If size has increased and + * the file is still valid, we sendmail it. + */ + + mailFd = open(mailFile, O_RDONLY); + remove(mailFile); + if (mailFd < 0) { + return; + } + + if (fstat(mailFd, &sbuf) < 0 || + sbuf.st_uid != DaemonUid || + sbuf.st_nlink != 0 || + sbuf.st_size == line->cl_MailPos || + !S_ISREG(sbuf.st_mode) + ) { + close(mailFd); + return; + } + + if ((line->cl_Pid = fork()) == 0) { + /* + * CHILD, FORK OK + */ + + /* + * change user id - no way in hell security can be compromised + * by the mailing and we already verified the mail file. + */ + + if (ChangeUser(file->cf_User, 1) < 0) + exit(0); + + /* + * run sendmail with mail file as standard input, only if + * mail file exists! + */ + + dup2(mailFd, 0); + dup2(1, 2); + close(mailFd); + + execl(SENDMAIL, SENDMAIL, SENDMAIL_ARGS, NULL, NULL); + log_err("unable to exec %s %s, user %s, output to sink null", + SENDMAIL, + SENDMAIL_ARGS, + file->cf_User + ); + exit(0); + } else if (line->cl_Pid < 0) { + /* + * PARENT, FORK FAILED + */ + log_err("unable to fork, user %s", file->cf_User); + line->cl_Pid = 0; + } else { + /* + * PARENT, FORK OK + */ + } + close(mailFd); +} diff --git a/miscutils/crontab.c b/miscutils/crontab.c new file mode 100644 index 000000000..c86990653 --- /dev/null +++ b/miscutils/crontab.c @@ -0,0 +1,393 @@ +/* + * CRONTAB + * + * usually setuid root, -c option only works if getuid() == geteuid() + * + * Copyright 1994 Matthew Dillon (dillon@apollo.west.oic.com) + * May be distributed under the GNU General Public License + * + * Vladimir Oleynik (C) 2002 to be used in busybox + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef CRONTABS +#define CRONTABS "/var/spool/cron/crontabs" +#endif +#ifndef TMPDIR +#define TMPDIR "/var/spool/cron" +#endif +#ifndef CRONUPDATE +#define CRONUPDATE "cron.update" +#endif +#ifndef PATH_VI +#define PATH_VI "/usr/bin/vi" /* location of vi */ +#endif + +#include "busybox.h" + +static const char *CDir = CRONTABS; + +static void EditFile(const char *user, const char *file); +static int GetReplaceStream(const char *user, const char *file); +static int ChangeUser(const char *user, short dochdir); + +int +crontab_main(int ac, char **av) +{ + enum { NONE, EDIT, LIST, REPLACE, DELETE } option = NONE; + const struct passwd *pas; + const char *repFile = NULL; + int repFd = 0; + int i; + char caller[256]; /* user that ran program */ + int UserId; + + UserId = getuid(); + if ((pas = getpwuid(UserId)) == NULL) + perror_msg_and_die("getpwuid"); + + strncpy(caller, pas->pw_name, sizeof(caller)); + + i = 1; + if (ac > 1) { + if (av[1][0] == '-' && av[1][1] == 0) { + option = REPLACE; + ++i; + } else if (av[1][0] != '-') { + option = REPLACE; + ++i; + repFile = av[1]; + } + } + + for (; i < ac; ++i) { + char *ptr = av[i]; + + if (*ptr != '-') + break; + ptr += 2; + + switch(ptr[-1]) { + case 'l': + if (ptr[-1] == 'l') + option = LIST; + /* fall through */ + case 'e': + if (ptr[-1] == 'e') + option = EDIT; + /* fall through */ + case 'd': + if (ptr[-1] == 'd') + option = DELETE; + /* fall through */ + case 'u': + if (i + 1 < ac && av[i+1][0] != '-') { + ++i; + if (getuid() == geteuid()) { + pas = getpwnam(av[i]); + if (pas) { + UserId = pas->pw_uid; + } else { + error_msg_and_die("user %s unknown", av[i]); + } + } else { + error_msg_and_die("only the superuser may specify a user"); + } + } + break; + case 'c': + if (getuid() == geteuid()) { + CDir = (*ptr) ? ptr : av[++i]; + } else { + error_msg_and_die("-c option: superuser only"); + } + break; + default: + i = ac; + break; + } + } + if (i != ac || option == NONE) + show_usage(); + + /* + * Get password entry + */ + + if ((pas = getpwuid(UserId)) == NULL) + perror_msg_and_die("getpwuid"); + + /* + * If there is a replacement file, obtain a secure descriptor to it. + */ + + if (repFile) { + repFd = GetReplaceStream(caller, repFile); + if (repFd < 0) + error_msg_and_die("unable to read replacement file"); + } + + /* + * Change directory to our crontab directory + */ + + if (chdir(CDir) < 0) + perror_msg_and_die("cannot change dir to %s", CDir); + + /* + * Handle options as appropriate + */ + + switch(option) { + case LIST: + { + FILE *fi; + char buf[1024]; + + if ((fi = fopen(pas->pw_name, "r"))) { + while (fgets(buf, sizeof(buf), fi) != NULL) + fputs(buf, stdout); + fclose(fi); + } else { + error_msg("no crontab for %s", pas->pw_name); + } + } + break; + case EDIT: + { + FILE *fi; + int fd; + int n; + char tmp[128]; + char buf[1024]; + + snprintf(tmp, sizeof(tmp), TMPDIR "/crontab.%d", getpid()); + if ((fd = open(tmp, O_RDWR|O_CREAT|O_TRUNC|O_EXCL, 0600)) >= 0) { + chown(tmp, getuid(), getgid()); + if ((fi = fopen(pas->pw_name, "r"))) { + while ((n = fread(buf, 1, sizeof(buf), fi)) > 0) + write(fd, buf, n); + } + EditFile(caller, tmp); + remove(tmp); + lseek(fd, 0L, 0); + repFd = fd; + } else { + error_msg_and_die("unable to create %s", tmp); + } + + } + option = REPLACE; + /* fall through */ + case REPLACE: + { + char buf[1024]; + char path[1024]; + int fd; + int n; + + snprintf(path, sizeof(path), "%s.new", pas->pw_name); + if ((fd = open(path, O_CREAT|O_TRUNC|O_EXCL|O_APPEND|O_WRONLY, 0600)) >= 0) { + while ((n = read(repFd, buf, sizeof(buf))) > 0) { + write(fd, buf, n); + } + close(fd); + rename(path, pas->pw_name); + } else { + error_msg("unable to create %s/%s", CDir, buf); + } + close(repFd); + } + break; + case DELETE: + remove(pas->pw_name); + break; + case NONE: + default: + break; + } + + /* + * Bump notification file. Handle window where crond picks file up + * before we can write our entry out. + */ + + if (option == REPLACE || option == DELETE) { + FILE *fo; + struct stat st; + + while ((fo = fopen(CRONUPDATE, "a"))) { + fprintf(fo, "%s\n", pas->pw_name); + fflush(fo); + if (fstat(fileno(fo), &st) != 0 || st.st_nlink != 0) { + fclose(fo); + break; + } + fclose(fo); + /* loop */ + } + if (fo == NULL) { + error_msg("unable to append to %s/%s", CDir, CRONUPDATE); + } + } + return 0; +} + +static int +GetReplaceStream(const char *user, const char *file) +{ + int filedes[2]; + int pid; + int fd; + int n; + char buf[1024]; + + if (pipe(filedes) < 0) { + perror("pipe"); + return(-1); + } + if ((pid = fork()) < 0) { + perror("fork"); + return(-1); + } + if (pid > 0) { + /* + * PARENT + */ + + close(filedes[1]); + if (read(filedes[0], buf, 1) != 1) { + close(filedes[0]); + filedes[0] = -1; + } + return(filedes[0]); + } + + /* + * CHILD + */ + + close(filedes[0]); + + if (ChangeUser(user, 0) < 0) + exit(0); + + fd = open(file, O_RDONLY); + if (fd < 0) { + error_msg("unable to open %s", file); + exit(0); + } + buf[0] = 0; + write(filedes[1], buf, 1); + while ((n = read(fd, buf, sizeof(buf))) > 0) { + write(filedes[1], buf, n); + } + exit(0); +} + +static void +EditFile(const char *user, const char *file) +{ + int pid; + + if ((pid = fork()) == 0) { + /* + * CHILD - change user and run editor + */ + char *ptr; + char visual[1024]; + + if (ChangeUser(user, 1) < 0) + exit(0); + if ((ptr = getenv("VISUAL")) == NULL || strlen(ptr) > 256) + ptr = PATH_VI; + + snprintf(visual, sizeof(visual), "%s %s", ptr, file); + execl("/bin/sh", "/bin/sh", "-c", visual, NULL); + perror("exec"); + exit(0); + } + if (pid < 0) { + /* + * PARENT - failure + */ + perror_msg_and_die("fork"); + } + wait4(pid, NULL, 0, NULL); +} + +static void +log(const char *ctl, ...) +{ + va_list va; + char buf[1024]; + + va_start(va, ctl); + vsnprintf(buf, sizeof(buf), ctl, va); + syslog(LOG_NOTICE, "%s",buf ); + va_end(va); +} + +static int +ChangeUser(const char *user, short dochdir) +{ + struct passwd *pas; + + /* + * Obtain password entry and change privilages + */ + + if ((pas = getpwnam(user)) == 0) { + log("failed to get uid for %s", user); + return(-1); + } + setenv("USER", pas->pw_name, 1); + setenv("HOME", pas->pw_dir, 1); + setenv("SHELL", "/bin/sh", 1); + + /* + * Change running state to the user in question + */ + + if (initgroups(user, pas->pw_gid) < 0) { + log("initgroups failed: %s %m", user); + return(-1); + } + if (setregid(pas->pw_gid, pas->pw_gid) < 0) { + log("setregid failed: %s %d", user, pas->pw_gid); + return(-1); + } + if (setreuid(pas->pw_uid, pas->pw_uid) < 0) { + log("setreuid failed: %s %d", user, pas->pw_uid); + return(-1); + } + if (dochdir) { + if (chdir(pas->pw_dir) < 0) { + if (chdir(TMPDIR) < 0) { + log("chdir failed: %s %s", user, pas->pw_dir); + log("chdir failed: %s " TMPDIR, user); + return(-1); + } + } + } + return(pas->pw_uid); +} -- cgit v1.2.3