]> CyberLeo.Net >> Repos - FreeBSD/stable/9.git/blob - usr.sbin/cron/crontab/crontab.c
MFC r363988:
[FreeBSD/stable/9.git] / usr.sbin / cron / crontab / crontab.c
1 /* Copyright 1988,1990,1993,1994 by Paul Vixie
2  * All rights reserved
3  *
4  * Distribute freely, except: don't remove my name from the source or
5  * documentation (don't take credit for my work), mark your changes (don't
6  * get me blamed for your possible bugs), don't alter or remove this
7  * notice.  May be sold if buildable source is provided to buyer.  No
8  * warrantee of any kind, express or implied, is included with this
9  * software; use at your own risk, responsibility for damages (if any) to
10  * anyone resulting from the use of this software rests entirely with the
11  * user.
12  *
13  * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
14  * I'll try to keep a version up to date.  I can be reached as follows:
15  * Paul Vixie          <paul@vix.com>          uunet!decwrl!vixie!paul
16  * From Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp
17  */
18
19 #if !defined(lint) && !defined(LINT)
20 static const char rcsid[] =
21   "$FreeBSD$";
22 #endif
23
24 /* crontab - install and manage per-user crontab files
25  * vix 02may87 [RCS has the rest of the log]
26  * vix 26jan87 [original]
27  */
28
29 #define MAIN_PROGRAM
30
31 #include <sys/param.h>
32 #include "cron.h"
33 #include <errno.h>
34 #include <fcntl.h>
35 #include <md5.h>
36 #include <paths.h>
37 #include <sys/file.h>
38 #include <sys/stat.h>
39 #ifdef USE_UTIMES
40 # include <sys/time.h>
41 #else
42 # include <time.h>
43 # include <utime.h>
44 #endif
45 #if defined(POSIX)
46 # include <locale.h>
47 #endif
48
49 #define MD5_SIZE 33
50 #define NHEADER_LINES 3
51
52
53 enum opt_t      { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
54
55 #if DEBUGGING
56 static char     *Options[] = { "???", "list", "delete", "edit", "replace" };
57 #endif
58
59
60 static  PID_T           Pid;
61 static  char            User[MAXLOGNAME], RealUser[MAXLOGNAME];
62 static  char            Filename[MAX_FNAME];
63 static  FILE            *NewCrontab;
64 static  int             CheckErrorCount;
65 static  enum opt_t      Option;
66 static  struct passwd   *pw;
67 static  void            list_cmd(void),
68                         delete_cmd(void),
69                         edit_cmd(void),
70                         poke_daemon(void),
71                         check_error(char *),
72                         parse_args(int c, char *v[]);
73 static  int             replace_cmd(void);
74
75
76 static void
77 usage(char *msg)
78 {
79         fprintf(stderr, "crontab: usage error: %s\n", msg);
80         fprintf(stderr, "%s\n%s\n",
81                 "usage: crontab [-u user] file",
82                 "       crontab [-u user] { -e | -l | -r }");
83         exit(ERROR_EXIT);
84 }
85
86
87 int
88 main(int argc, char *argv[])
89 {
90         int     exitstatus;
91
92         Pid = getpid();
93         ProgramName = argv[0];
94
95 #if defined(POSIX)
96         setlocale(LC_ALL, "");
97 #endif
98
99 #if defined(BSD)
100         setlinebuf(stderr);
101 #endif
102         parse_args(argc, argv);         /* sets many globals, opens a file */
103         set_cron_uid();
104         set_cron_cwd();
105         if (!allowed(User)) {
106                 warnx("you (%s) are not allowed to use this program", User);
107                 log_it(RealUser, Pid, "AUTH", "crontab command not allowed");
108                 exit(ERROR_EXIT);
109         }
110         exitstatus = OK_EXIT;
111         switch (Option) {
112         case opt_list:          list_cmd();
113                                 break;
114         case opt_delete:        delete_cmd();
115                                 break;
116         case opt_edit:          edit_cmd();
117                                 break;
118         case opt_replace:       if (replace_cmd() < 0)
119                                         exitstatus = ERROR_EXIT;
120                                 break;
121         case opt_unknown:
122                                 break;
123         }
124         exit(exitstatus);
125         /*NOTREACHED*/
126 }
127
128
129 static void
130 parse_args(argc, argv)
131         int     argc;
132         char    *argv[];
133 {
134         int             argch;
135         char            resolved_path[PATH_MAX];
136
137         if (!(pw = getpwuid(getuid())))
138                 errx(ERROR_EXIT, "your UID isn't in the passwd file, bailing out");
139         bzero(pw->pw_passwd, strlen(pw->pw_passwd));
140         (void) strncpy(User, pw->pw_name, (sizeof User)-1);
141         User[(sizeof User)-1] = '\0';
142         strcpy(RealUser, User);
143         Filename[0] = '\0';
144         Option = opt_unknown;
145         while ((argch = getopt(argc, argv, "u:lerx:")) != -1) {
146                 switch (argch) {
147                 case 'x':
148                         if (!set_debug_flags(optarg))
149                                 usage("bad debug option");
150                         break;
151                 case 'u':
152                         if (getuid() != ROOT_UID)
153                                 errx(ERROR_EXIT, "must be privileged to use -u");
154                         if (!(pw = getpwnam(optarg)))
155                                 errx(ERROR_EXIT, "user `%s' unknown", optarg);
156                         bzero(pw->pw_passwd, strlen(pw->pw_passwd));
157                         (void) strncpy(User, pw->pw_name, (sizeof User)-1);
158                         User[(sizeof User)-1] = '\0';
159                         break;
160                 case 'l':
161                         if (Option != opt_unknown)
162                                 usage("only one operation permitted");
163                         Option = opt_list;
164                         break;
165                 case 'r':
166                         if (Option != opt_unknown)
167                                 usage("only one operation permitted");
168                         Option = opt_delete;
169                         break;
170                 case 'e':
171                         if (Option != opt_unknown)
172                                 usage("only one operation permitted");
173                         Option = opt_edit;
174                         break;
175                 default:
176                         usage("unrecognized option");
177                 }
178         }
179
180         endpwent();
181
182         if (Option != opt_unknown) {
183                 if (argv[optind] != NULL) {
184                         usage("no arguments permitted after this option");
185                 }
186         } else {
187                 if (argv[optind] != NULL) {
188                         Option = opt_replace;
189                         (void) strncpy (Filename, argv[optind], (sizeof Filename)-1);
190                         Filename[(sizeof Filename)-1] = '\0';
191
192                 } else {
193                         usage("file name must be specified for replace");
194                 }
195         }
196
197         if (Option == opt_replace) {
198                 /* relinquish the setuid status of the binary during
199                  * the open, lest nonroot users read files they should
200                  * not be able to read.  we can't use access() here
201                  * since there's a race condition.  thanks go out to
202                  * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
203                  * the race.
204                  */
205
206                 if (swap_uids() < OK)
207                         err(ERROR_EXIT, "swapping uids");
208
209                 /* we have to open the file here because we're going to
210                  * chdir(2) into /var/cron before we get around to
211                  * reading the file.
212                  */
213                 if (!strcmp(Filename, "-")) {
214                         NewCrontab = stdin;
215                 } else if (realpath(Filename, resolved_path) != NULL &&
216                     !strcmp(resolved_path, SYSCRONTAB)) {
217                         err(ERROR_EXIT, SYSCRONTAB " must be edited manually");
218                 } else {
219                         if (!(NewCrontab = fopen(Filename, "r")))
220                                 err(ERROR_EXIT, "%s", Filename);
221                 }
222                 if (swap_uids_back() < OK)
223                         err(ERROR_EXIT, "swapping uids back");
224         }
225
226         Debug(DMISC, ("user=%s, file=%s, option=%s\n",
227                       User, Filename, Options[(int)Option]))
228 }
229
230 static void
231 copy_file(FILE *in, FILE *out) {
232         int     x, ch;
233
234         Set_LineNum(1)
235         /* ignore the top few comments since we probably put them there.
236          */
237         for (x = 0;  x < NHEADER_LINES;  x++) {
238                 ch = get_char(in);
239                 if (EOF == ch)
240                         break;
241                 if ('#' != ch) {
242                         putc(ch, out);
243                         break;
244                 }
245                 while (EOF != (ch = get_char(in)))
246                         if (ch == '\n')
247                                 break;
248                 if (EOF == ch)
249                         break;
250         }
251
252         /* copy the rest of the crontab (if any) to the output file.
253          */
254         if (EOF != ch)
255                 while (EOF != (ch = get_char(in)))
256                         putc(ch, out);
257 }
258
259 static void
260 list_cmd() {
261         char    n[MAX_FNAME];
262         FILE    *f;
263
264         log_it(RealUser, Pid, "LIST", User);
265         (void) snprintf(n, sizeof(n), CRON_TAB(User));
266         if (!(f = fopen(n, "r"))) {
267                 if (errno == ENOENT)
268                         errx(ERROR_EXIT, "no crontab for %s", User);
269                 else
270                         err(ERROR_EXIT, "%s", n);
271         }
272
273         /* file is open. copy to stdout, close.
274          */
275         copy_file(f, stdout);
276         fclose(f);
277 }
278
279
280 static void
281 delete_cmd() {
282         char    n[MAX_FNAME];
283         int ch, first;
284
285         if (isatty(STDIN_FILENO)) {
286                 (void)fprintf(stderr, "remove crontab for %s? ", User);
287                 first = ch = getchar();
288                 while (ch != '\n' && ch != EOF)
289                         ch = getchar();
290                 if (first != 'y' && first != 'Y')
291                         return;
292         }
293
294         log_it(RealUser, Pid, "DELETE", User);
295         (void) snprintf(n, sizeof(n), CRON_TAB(User));
296         if (unlink(n)) {
297                 if (errno == ENOENT)
298                         errx(ERROR_EXIT, "no crontab for %s", User);
299                 else
300                         err(ERROR_EXIT, "%s", n);
301         }
302         poke_daemon();
303 }
304
305
306 static void
307 check_error(msg)
308         char    *msg;
309 {
310         CheckErrorCount++;
311         fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
312 }
313
314
315 static void
316 edit_cmd() {
317         char            n[MAX_FNAME], q[MAX_TEMPSTR], *editor;
318         FILE            *f;
319         int             t;
320         struct stat     statbuf, fsbuf;
321         WAIT_T          waiter;
322         PID_T           pid, xpid;
323         mode_t          um;
324         int             syntax_error = 0;
325         char            orig_md5[MD5_SIZE];
326         char            new_md5[MD5_SIZE];
327
328         log_it(RealUser, Pid, "BEGIN EDIT", User);
329         (void) snprintf(n, sizeof(n), CRON_TAB(User));
330         if (!(f = fopen(n, "r"))) {
331                 if (errno != ENOENT)
332                         err(ERROR_EXIT, "%s", n);
333                 warnx("no crontab for %s - using an empty one", User);
334                 if (!(f = fopen(_PATH_DEVNULL, "r")))
335                         err(ERROR_EXIT, _PATH_DEVNULL);
336         }
337
338         um = umask(077);
339         (void) snprintf(Filename, sizeof(Filename), "/tmp/crontab.XXXXXXXXXX");
340         if ((t = mkstemp(Filename)) == -1) {
341                 warn("%s", Filename);
342                 (void) umask(um);
343                 goto fatal;
344         }
345         (void) umask(um);
346 #ifdef HAS_FCHOWN
347         if (fchown(t, getuid(), getgid()) < 0) {
348 #else
349         if (chown(Filename, getuid(), getgid()) < 0) {
350 #endif
351                 warn("fchown");
352                 goto fatal;
353         }
354         if (!(NewCrontab = fdopen(t, "r+"))) {
355                 warn("fdopen");
356                 goto fatal;
357         }
358
359         copy_file(f, NewCrontab);
360         fclose(f);
361         if (fflush(NewCrontab))
362                 err(ERROR_EXIT, "%s", Filename);
363         if (fstat(t, &fsbuf) < 0) {
364                 warn("unable to fstat temp file");
365                 goto fatal;
366         }
367  again:
368         if (swap_uids() < OK)
369                 err(ERROR_EXIT, "swapping uids");
370         if (stat(Filename, &statbuf) < 0) {
371                 warn("stat");
372  fatal:         unlink(Filename);
373                 exit(ERROR_EXIT);
374         }
375         if (swap_uids_back() < OK)
376                 err(ERROR_EXIT, "swapping uids back");
377         if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
378                 errx(ERROR_EXIT, "temp file must be edited in place");
379         if (MD5File(Filename, orig_md5) == NULL) {
380                 warn("MD5");
381                 goto fatal;
382         }
383
384         if ((!(editor = getenv("VISUAL")))
385          && (!(editor = getenv("EDITOR")))
386             ) {
387                 editor = EDITOR;
388         }
389
390         /* we still have the file open.  editors will generally rewrite the
391          * original file rather than renaming/unlinking it and starting a
392          * new one; even backup files are supposed to be made by copying
393          * rather than by renaming.  if some editor does not support this,
394          * then don't use it.  the security problems are more severe if we
395          * close and reopen the file around the edit.
396          */
397
398         switch (pid = fork()) {
399         case -1:
400                 warn("fork");
401                 goto fatal;
402         case 0:
403                 /* child */
404                 if (setuid(getuid()) < 0)
405                         err(ERROR_EXIT, "setuid(getuid())");
406                 if (chdir("/tmp") < 0)
407                         err(ERROR_EXIT, "chdir(/tmp)");
408                 if (strlen(editor) + strlen(Filename) + 2 >= MAX_TEMPSTR)
409                         errx(ERROR_EXIT, "editor or filename too long");
410                 execlp(editor, editor, Filename, (char *)NULL);
411                 err(ERROR_EXIT, "%s", editor);
412                 /*NOTREACHED*/
413         default:
414                 /* parent */
415                 break;
416         }
417
418         /* parent */
419         {
420         void (*sig[3])(int signal);
421         sig[0] = signal(SIGHUP, SIG_IGN);
422         sig[1] = signal(SIGINT, SIG_IGN);
423         sig[2] = signal(SIGTERM, SIG_IGN);
424         xpid = wait(&waiter);
425         signal(SIGHUP, sig[0]);
426         signal(SIGINT, sig[1]);
427         signal(SIGTERM, sig[2]);
428         }
429         if (xpid != pid) {
430                 warnx("wrong PID (%d != %d) from \"%s\"", xpid, pid, editor);
431                 goto fatal;
432         }
433         if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) {
434                 warnx("\"%s\" exited with status %d", editor, WEXITSTATUS(waiter));
435                 goto fatal;
436         }
437         if (WIFSIGNALED(waiter)) {
438                 warnx("\"%s\" killed; signal %d (%score dumped)",
439                         editor, WTERMSIG(waiter), WCOREDUMP(waiter) ?"" :"no ");
440                 goto fatal;
441         }
442         if (swap_uids() < OK)
443                 err(ERROR_EXIT, "swapping uids");
444         if (stat(Filename, &statbuf) < 0) {
445                 warn("stat");
446                 goto fatal;
447         }
448         if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
449                 errx(ERROR_EXIT, "temp file must be edited in place");
450         if (MD5File(Filename, new_md5) == NULL) {
451                 warn("MD5");
452                 goto fatal;
453         }
454         if (swap_uids_back() < OK)
455                 err(ERROR_EXIT, "swapping uids back");
456         if (strcmp(orig_md5, new_md5) == 0 && !syntax_error) {
457                 warnx("no changes made to crontab");
458                 goto remove;
459         }
460         warnx("installing new crontab");
461         switch (replace_cmd()) {
462         case 0:                 /* Success */
463                 break;
464         case -1:                /* Syntax error */
465                 for (;;) {
466                         printf("Do you want to retry the same edit? ");
467                         fflush(stdout);
468                         q[0] = '\0';
469                         (void) fgets(q, sizeof q, stdin);
470                         switch (islower(q[0]) ? q[0] : tolower(q[0])) {
471                         case 'y':
472                                 syntax_error = 1;
473                                 goto again;
474                         case 'n':
475                                 goto abandon;
476                         default:
477                                 fprintf(stderr, "Enter Y or N\n");
478                         }
479                 }
480                 /*NOTREACHED*/
481         case -2:                /* Install error */
482         abandon:
483                 warnx("edits left in %s", Filename);
484                 goto done;
485         default:
486                 warnx("panic: bad switch() in replace_cmd()");
487                 goto fatal;
488         }
489  remove:
490         unlink(Filename);
491  done:
492         log_it(RealUser, Pid, "END EDIT", User);
493 }
494
495
496 /* returns      0       on success
497  *              -1      on syntax error
498  *              -2      on install error
499  */
500 static int
501 replace_cmd() {
502         char    n[MAX_FNAME], envstr[MAX_ENVSTR], tn[MAX_FNAME];
503         FILE    *tmp;
504         int     ch, eof;
505         entry   *e;
506         time_t  now = time(NULL);
507         char    **envp = env_init();
508
509         if (envp == NULL) {
510                 warnx("cannot allocate memory");
511                 return (-2);
512         }
513
514         (void) snprintf(n, sizeof(n), "tmp.%d", Pid);
515         (void) snprintf(tn, sizeof(tn), CRON_TAB(n));
516
517         if (!(tmp = fopen(tn, "w+"))) {
518                 warn("%s", tn);
519                 return (-2);
520         }
521
522         /* write a signature at the top of the file.
523          *
524          * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
525          */
526         fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
527         fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
528         fprintf(tmp, "# (Cron version -- %s)\n", rcsid);
529
530         /* copy the crontab to the tmp
531          */
532         rewind(NewCrontab);
533         Set_LineNum(1)
534         while (EOF != (ch = get_char(NewCrontab)))
535                 putc(ch, tmp);
536         ftruncate(fileno(tmp), ftello(tmp));
537         fflush(tmp);  rewind(tmp);
538
539         if (ferror(tmp)) {
540                 warnx("error while writing new crontab to %s", tn);
541                 fclose(tmp);  unlink(tn);
542                 return (-2);
543         }
544
545         /* check the syntax of the file being installed.
546          */
547
548         /* BUG: was reporting errors after the EOF if there were any errors
549          * in the file proper -- kludged it by stopping after first error.
550          *              vix 31mar87
551          */
552         Set_LineNum(1 - NHEADER_LINES)
553         CheckErrorCount = 0;  eof = FALSE;
554         while (!CheckErrorCount && !eof) {
555                 switch (load_env(envstr, tmp)) {
556                 case ERR:
557                         eof = TRUE;
558                         break;
559                 case FALSE:
560                         e = load_entry(tmp, check_error, pw, envp);
561                         if (e)
562                                 free_entry(e);
563                         break;
564                 case TRUE:
565                         break;
566                 }
567         }
568
569         if (CheckErrorCount != 0) {
570                 warnx("errors in crontab file, can't install");
571                 fclose(tmp);  unlink(tn);
572                 return (-1);
573         }
574
575 #ifdef HAS_FCHOWN
576         if (fchown(fileno(tmp), ROOT_UID, -1) < OK)
577 #else
578         if (chown(tn, ROOT_UID, -1) < OK)
579 #endif
580         {
581                 warn("chown");
582                 fclose(tmp);  unlink(tn);
583                 return (-2);
584         }
585
586 #ifdef HAS_FCHMOD
587         if (fchmod(fileno(tmp), 0600) < OK)
588 #else
589         if (chmod(tn, 0600) < OK)
590 #endif
591         {
592                 warn("chown");
593                 fclose(tmp);  unlink(tn);
594                 return (-2);
595         }
596
597         if (fclose(tmp) == EOF) {
598                 warn("fclose");
599                 unlink(tn);
600                 return (-2);
601         }
602
603         (void) snprintf(n, sizeof(n), CRON_TAB(User));
604         if (rename(tn, n)) {
605                 warn("error renaming %s to %s", tn, n);
606                 unlink(tn);
607                 return (-2);
608         }
609
610         log_it(RealUser, Pid, "REPLACE", User);
611
612         /*
613          * Creating the 'tn' temp file has already updated the
614          * modification time of the spool directory.  Sleep for a
615          * second to ensure that poke_daemon() sets a later
616          * modification time.  Otherwise, this can race with the cron
617          * daemon scanning for updated crontabs.
618          */
619         sleep(1);
620
621         poke_daemon();
622
623         return (0);
624 }
625
626
627 static void
628 poke_daemon() {
629 #ifdef USE_UTIMES
630         struct timeval tvs[2];
631         struct timezone tz;
632
633         (void) gettimeofday(&tvs[0], &tz);
634         tvs[1] = tvs[0];
635         if (utimes(SPOOL_DIR, tvs) < OK) {
636                 warn("can't update mtime on spooldir %s", SPOOL_DIR);
637                 return;
638         }
639 #else
640         if (utime(SPOOL_DIR, NULL) < OK) {
641                 warn("can't update mtime on spooldir %s", SPOOL_DIR);
642                 return;
643         }
644 #endif /*USE_UTIMES*/
645 }