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