5 kx
5 kx /*
5 kx * CRONTAB.C
5 kx *
5 kx * crontab [-u user] [-c dir] [-l|-e|-d|file|-]
5 kx * usually run as setuid root
5 kx * -u and -c options only work if getuid() == geteuid()
5 kx *
5 kx * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com)
5 kx * Copyright 2009-2011 James Pryor <profjim@jimpryor.net>
5 kx * May be distributed under the GNU General Public License
5 kx */
5 kx
5 kx #include "defs.h"
5 kx
5 kx Prototype void printlogf(int level, const char *ctl, ...);
5 kx
5 kx void Usage(void);
5 kx int GetReplaceStream(const char *user, const char *file);
5 kx void EditFile(const char *user, const char *file);
5 kx
5 kx const char *CDir = CRONTABS;
5 kx int UserId;
5 kx
5 kx
5 kx int
5 kx main(int ac, char **av)
5 kx {
5 kx enum { NONE, EDIT, LIST, REPLACE, DELETE } option = NONE;
5 kx struct passwd *pas;
5 kx char *repFile = NULL;
5 kx int repFd = 0;
5 kx int i;
5 kx char caller[SMALL_BUFFER]; /* user that ran program */
5 kx
5 kx UserId = getuid();
5 kx if ((pas = getpwuid(UserId)) == NULL) {
5 kx perror("getpwuid");
5 kx exit(1);
5 kx }
5 kx /* [v]snprintf write at most size including \0; they'll null-terminate, even when they truncate */
5 kx /* return value >= size means result was truncated */
5 kx if (snprintf(caller, sizeof(caller), "%s", pas->pw_name) >= sizeof(caller)) {
5 kx printlogf(0, "username '%s' too long", caller);
5 kx exit(1);
5 kx }
5 kx
5 kx opterr = 0;
5 kx while ((i=getopt(ac,av,"ledu:c:")) != -1) {
5 kx switch(i) {
5 kx case 'l':
5 kx if (option != NONE)
5 kx Usage();
5 kx else
5 kx option = LIST;
5 kx break;
5 kx case 'e':
5 kx if (option != NONE)
5 kx Usage();
5 kx else
5 kx option = EDIT;
5 kx break;
5 kx case 'd':
5 kx if (option != NONE)
5 kx Usage();
5 kx else
5 kx option = DELETE;
5 kx break;
5 kx case 'u':
5 kx /* getopt guarantees optarg != 0 here */
5 kx if (*optarg != 0 && getuid() == geteuid()) {
5 kx pas = getpwnam(optarg);
5 kx if (pas) {
5 kx UserId = pas->pw_uid;
5 kx /* paranoia */
5 kx if ((pas = getpwuid(UserId)) == NULL) {
5 kx perror("getpwuid");
5 kx exit(1);
5 kx }
5 kx } else {
5 kx printlogf(0, "user '%s' unknown", optarg);
5 kx exit(1);
5 kx }
5 kx } else {
5 kx printlogf(0, "-u option: superuser only");
5 kx exit(1);
5 kx }
5 kx break;
5 kx case 'c':
5 kx /* getopt guarantees optarg != 0 here */
5 kx if (*optarg != 0 && getuid() == geteuid()) {
5 kx CDir = optarg;
5 kx } else {
5 kx printlogf(0, "-c option: superuser only");
5 kx exit(1);
5 kx }
5 kx break;
5 kx default:
5 kx /* unrecognized -X */
5 kx option = NONE;
5 kx }
5 kx }
5 kx
5 kx if (option == NONE && optind == ac - 1) {
5 kx if (av[optind][0] != '-') {
5 kx option = REPLACE;
5 kx repFile = av[optind];
5 kx optind++;
5 kx } else if (av[optind][1] == 0) {
5 kx option = REPLACE;
5 kx optind++;
5 kx }
5 kx }
5 kx if (option == NONE || optind != ac) {
5 kx Usage();
5 kx }
5 kx
5 kx /*
5 kx * If there is a replacement file, obtain a secure descriptor to it.
5 kx */
5 kx
5 kx if (repFile) {
5 kx repFd = GetReplaceStream(caller, repFile);
5 kx if (repFd < 0) {
5 kx printlogf(0, "unable to read replacement file %s", repFile);
5 kx exit(1);
5 kx }
5 kx }
5 kx
5 kx /*
5 kx * Change directory to our crontab directory
5 kx */
5 kx
5 kx if (chdir(CDir) < 0) {
5 kx printlogf(0, "cannot change dir to %s: %s", CDir, strerror(errno));
5 kx exit(1);
5 kx }
5 kx
5 kx /*
5 kx * Handle options as appropriate
5 kx */
5 kx
5 kx switch(option) {
5 kx case LIST:
5 kx {
5 kx FILE *fi;
5 kx char buf[RW_BUFFER];
5 kx
5 kx if ((fi = fopen(pas->pw_name, "r"))) {
5 kx while (fgets(buf, sizeof(buf), fi) != NULL)
5 kx fputs(buf, stdout);
5 kx fclose(fi);
5 kx } else {
5 kx fprintf(stderr, "no crontab for %s\n", pas->pw_name);
5 kx /* no error code */
5 kx }
5 kx }
5 kx break;
5 kx case EDIT:
5 kx {
5 kx FILE *fi;
5 kx int fd;
5 kx int n;
5 kx char tmp[] = TMPDIR "/crontab.XXXXXX";
5 kx char buf[RW_BUFFER];
5 kx
5 kx /*
5 kx * Create temp file with perm 0600 and O_EXCL flag, ensuring that this call creates the file
5 kx * Read from fi for "$CDir/$USER", write to fd for temp file
5 kx * EditFile changes user if necessary, and runs editor on temp file
5 kx * Then we delete the temp file, keeping its fd as repFd
5 kx */
5 kx if ((fd = mkstemp(tmp)) >= 0) {
5 kx chown(tmp, getuid(), getgid());
5 kx if ((fi = fopen(pas->pw_name, "r"))) {
5 kx while ((n = fread(buf, 1, sizeof(buf), fi)) > 0)
5 kx write(fd, buf, n);
5 kx }
5 kx EditFile(caller, tmp);
5 kx remove(tmp);
5 kx lseek(fd, 0L, 0);
5 kx repFd = fd;
5 kx } else {
5 kx printlogf(0, "unable to create %s: %s", tmp, strerror(errno));
5 kx exit(1);
5 kx }
5 kx
5 kx }
5 kx option = REPLACE;
5 kx /* fall through */
5 kx case REPLACE:
5 kx {
5 kx char buf[RW_BUFFER];
5 kx char path[SMALL_BUFFER];
5 kx int fd;
5 kx int n;
5 kx
5 kx /*
5 kx * Read from repFd, write to fd for "$CDir/$USER.new"
5 kx */
5 kx snprintf(path, sizeof(path), "%s.new", pas->pw_name);
5 kx if ((fd = open(path, O_CREAT|O_TRUNC|O_APPEND|O_WRONLY, 0600)) >= 0) {
5 kx while ((n = read(repFd, buf, sizeof(buf))) > 0) {
5 kx write(fd, buf, n);
5 kx }
5 kx close(fd);
5 kx rename(path, pas->pw_name);
5 kx } else {
5 kx fprintf(stderr, "unable to create %s/%s: %s\n",
5 kx CDir,
5 kx path,
5 kx strerror(errno)
5 kx );
5 kx }
5 kx close(repFd);
5 kx }
5 kx break;
5 kx case DELETE:
5 kx remove(pas->pw_name);
5 kx break;
5 kx case NONE:
5 kx default:
5 kx break;
5 kx }
5 kx
5 kx /*
5 kx * Bump notification file. Handle window where crond picks file up
5 kx * before we can write our entry out.
5 kx */
5 kx
5 kx if (option == REPLACE || option == DELETE) {
5 kx FILE *fo;
5 kx struct stat st;
5 kx
5 kx while ((fo = fopen(CRONUPDATE, "a"))) {
5 kx fprintf(fo, "%s\n", pas->pw_name);
5 kx fflush(fo);
5 kx if (fstat(fileno(fo), &st) != 0 || st.st_nlink != 0) {
5 kx fclose(fo);
5 kx break;
5 kx }
5 kx fclose(fo);
5 kx /* loop */
5 kx }
5 kx if (fo == NULL) {
5 kx fprintf(stderr, "unable to append to %s/%s\n", CDir, CRONUPDATE);
5 kx }
5 kx }
5 kx exit(0);
5 kx /* not reached */
5 kx }
5 kx
5 kx void
5 kx printlogf(int level, const char *ctl, ...)
5 kx {
5 kx va_list va;
5 kx char buf[LOG_BUFFER];
5 kx
5 kx va_start(va, ctl);
5 kx vsnprintf(buf, sizeof(buf), ctl, va);
5 kx write(2, buf, strlen(buf));
5 kx va_end(va);
5 kx }
5 kx
5 kx void
5 kx Usage(void)
5 kx {
5 kx /*
5 kx * parse error
5 kx */
5 kx printf("crontab " VERSION "\n");
5 kx printf("crontab file [-u user] replace crontab from file\n");
5 kx printf("crontab - [-u user] replace crontab from stdin\n");
5 kx printf("crontab -l [-u user] list crontab\n");
5 kx printf("crontab -e [-u user] edit crontab\n");
5 kx printf("crontab -d [-u user] delete crontab\n");
5 kx printf("crontab -c dir <opts> specify crontab directory\n");
5 kx exit(2);
5 kx }
5 kx
5 kx int
5 kx GetReplaceStream(const char *user, const char *file)
5 kx {
5 kx int filedes[2];
5 kx int pid;
5 kx int fd;
5 kx int n;
5 kx char buf[RW_BUFFER];
5 kx
5 kx if (pipe(filedes) < 0) {
5 kx perror("pipe");
5 kx return(-1);
5 kx }
5 kx if ((pid = fork()) < 0) {
5 kx perror("fork");
5 kx return(-1);
5 kx }
5 kx if (pid > 0) {
5 kx /*
5 kx * PARENT
5 kx * Read from pipe[0], return it (or -1 if it's empty)
5 kx */
5 kx
5 kx close(filedes[1]);
5 kx if (read(filedes[0], buf, 1) != 1) {
5 kx close(filedes[0]);
5 kx filedes[0] = -1;
5 kx }
5 kx return(filedes[0]);
5 kx }
5 kx
5 kx /*
5 kx * CHILD
5 kx * Read from fd for "$file", write to pipe[1]
5 kx */
5 kx
5 kx close(filedes[0]);
5 kx
5 kx if (ChangeUser(user, NULL) < 0)
5 kx exit(0);
5 kx
5 kx fd = open(file, O_RDONLY);
5 kx if (fd < 0) {
5 kx printlogf(0, "unable to open %s: %s", file, strerror(errno));
5 kx exit(1);
5 kx }
5 kx buf[0] = 0;
5 kx write(filedes[1], buf, 1);
5 kx while ((n = read(fd, buf, sizeof(buf))) > 0) {
5 kx write(filedes[1], buf, n);
5 kx }
5 kx exit(0);
5 kx }
5 kx
5 kx void
5 kx EditFile(const char *user, const char *file)
5 kx {
5 kx int pid;
5 kx
5 kx if ((pid = fork()) == 0) {
5 kx /*
5 kx * CHILD - change user and run editor on "$file"
5 kx */
5 kx const char *ptr;
5 kx char visual[SMALL_BUFFER];
5 kx
5 kx if (ChangeUser(user, TMPDIR) < 0)
5 kx exit(0);
5 kx if ((ptr = getenv("EDITOR")) == NULL || strlen(ptr) >= sizeof(visual))
5 kx if ((ptr = getenv("VISUAL")) == NULL || strlen(ptr) >= sizeof(visual))
5 kx ptr = PATH_VI;
5 kx
5 kx /* [v]snprintf write at most size including \0; they'll null-terminate, even when they truncate */
5 kx /* return value >= size means result was truncated */
5 kx if (snprintf(visual, sizeof(visual), "%s %s", ptr, file) < sizeof(visual))
5 kx execl("/bin/sh", "/bin/sh", "-c", visual, NULL);
5 kx printlogf(0, "couldn't exec %s", visual);
5 kx exit(1);
5 kx }
5 kx if (pid < 0) {
5 kx /*
5 kx * PARENT - failure
5 kx */
5 kx perror("fork");
5 kx exit(1);
5 kx }
5 kx waitpid(pid, NULL, 0);
5 kx }
5 kx