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