1 /* $OpenBSD: crontab.c,v 1.48 2005/01/30 21:00:31 millert Exp $ */
2
3 /* Copyright 1988,1990,1993,1994 by Paul Vixie
4 * All rights reserved
5 */
6
7 /*
8 * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
9 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
10 *
11 * Permission to use, copy, modify, and distribute this software for any
12 * purpose with or without fee is hereby granted, provided that the above
13 * copyright notice and this permission notice appear in all copies.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
16 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR
18 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
21 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22 */
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 "cron.h"
32
33 __RCSID("$MirOS: src/usr.sbin/cron/crontab.c,v 1.4 2007/07/05 23:09:47 tg Exp $");
34
35 #define NHEADER_LINES 3
36
37 enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
38
39 #if DEBUGGING
40 static char *Options[] = { "???", "list", "delete", "edit", "replace" };
41 static char *getoptargs = "u:lerx:";
42 #else
43 static char *getoptargs = "u:ler";
44 #endif
45
46 static PID_T Pid;
47 static char User[MAX_UNAME], RealUser[MAX_UNAME];
48 static char Filename[MAX_FNAME], TempFilename[MAX_FNAME];
49 static FILE *NewCrontab;
50 static int CheckErrorCount;
51 static enum opt_t Option;
52 static struct passwd *pw;
53 static void list_cmd(void),
54 delete_cmd(void),
55 edit_cmd(void),
56 check_error(const char *),
57 parse_args(int c, char *v[]),
58 die(int);
59 static int replace_cmd(void);
60
61 static void
usage(const char * msg)62 usage(const char *msg) {
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 int
main(int argc,char * argv[])74 main(int argc, char *argv[]) {
75 int exitstatus;
76
77 Pid = getpid();
78 ProgramName = argv[0];
79
80 #ifndef __MirBSD__
81 setlocale(LC_ALL, "");
82 #endif
83
84 #if defined(BSD)
85 setlinebuf(stderr);
86 #endif
87 parse_args(argc, argv); /* sets many globals, opens a file */
88 set_cron_cwd();
89 if (!allowed(RealUser, CRON_ALLOW, CRON_DENY)) {
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:
100 list_cmd();
101 break;
102 case opt_delete:
103 delete_cmd();
104 break;
105 case opt_edit:
106 edit_cmd();
107 break;
108 case opt_replace:
109 if (replace_cmd() < 0)
110 exitstatus = ERROR_EXIT;
111 break;
112 default:
113 exitstatus = ERROR_EXIT;
114 break;
115 }
116 exit(exitstatus);
117 /*NOTREACHED*/
118 }
119
120 static void
parse_args(int argc,char * argv[])121 parse_args(int argc, char *argv[]) {
122 int argch;
123
124 if (!(pw = getpwuid(getuid()))) {
125 fprintf(stderr, "%s: your UID isn't in the passwd file.\n",
126 ProgramName);
127 fprintf(stderr, "bailing out.\n");
128 exit(ERROR_EXIT);
129 }
130 if (strlen(pw->pw_name) >= sizeof User) {
131 fprintf(stderr, "username too long\n");
132 exit(ERROR_EXIT);
133 }
134 strlcpy(User, pw->pw_name, sizeof(User));
135 strlcpy(RealUser, User, sizeof(RealUser));
136 Filename[0] = '\0';
137 Option = opt_unknown;
138 while (-1 != (argch = getopt(argc, argv, getoptargs))) {
139 switch (argch) {
140 #if DEBUGGING
141 case 'x':
142 if (!set_debug_flags(optarg))
143 usage("bad debug option");
144 break;
145 #endif
146 case 'u':
147 if (MY_UID(pw) != ROOT_UID) {
148 fprintf(stderr,
149 "must be privileged to use -u\n");
150 exit(ERROR_EXIT);
151 }
152 if (!(pw = getpwnam(optarg))) {
153 fprintf(stderr, "%s: user `%s' unknown\n",
154 ProgramName, optarg);
155 exit(ERROR_EXIT);
156 }
157 if (strlcpy(User, optarg, sizeof User) >= sizeof User)
158 usage("username too long");
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 } else {
186 if (argv[optind] != NULL) {
187 Option = opt_replace;
188 if (strlcpy(Filename, argv[optind], sizeof Filename)
189 >= sizeof Filename)
190 usage("filename too long");
191 } else
192 usage("file name must be specified for replace");
193 }
194
195 if (Option == opt_replace) {
196 /* we have to open the file here because we're going to
197 * chdir(2) into /var/cron before we get around to
198 * reading the file.
199 */
200 if (!strcmp(Filename, "-"))
201 NewCrontab = stdin;
202 else {
203 /* relinquish the setgid status of the binary during
204 * the open, lest nonroot users read files they should
205 * not be able to read. we can't use access() here
206 * since there's a race condition. thanks go out to
207 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
208 * the race.
209 */
210
211 if (swap_gids() < OK) {
212 perror("swapping gids");
213 exit(ERROR_EXIT);
214 }
215 if (!(NewCrontab = fopen(Filename, "r"))) {
216 perror(Filename);
217 exit(ERROR_EXIT);
218 }
219 if (swap_gids_back() < OK) {
220 perror("swapping gids back");
221 exit(ERROR_EXIT);
222 }
223 }
224 }
225
226 Debug(DMISC, ("user=%s, file=%s, option=%s\n",
227 User, Filename, Options[(int)Option]))
228 }
229
230 static void
list_cmd(void)231 list_cmd(void) {
232 char n[MAX_FNAME];
233 FILE *f;
234 int ch;
235
236 log_it(RealUser, Pid, "LIST", User);
237 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
238 fprintf(stderr, "path too long\n");
239 exit(ERROR_EXIT);
240 }
241 if (!(f = fopen(n, "r"))) {
242 if (errno == ENOENT)
243 fprintf(stderr, "no crontab for %s\n", User);
244 else
245 perror(n);
246 exit(ERROR_EXIT);
247 }
248
249 /* file is open. copy to stdout, close.
250 */
251 Set_LineNum(1)
252 while (EOF != (ch = get_char(f)))
253 putchar(ch);
254 fclose(f);
255 }
256
257 static void
delete_cmd(void)258 delete_cmd(void) {
259 char n[MAX_FNAME];
260
261 log_it(RealUser, Pid, "DELETE", User);
262 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
263 fprintf(stderr, "path too long\n");
264 exit(ERROR_EXIT);
265 }
266 if (unlink(n) != 0) {
267 if (errno == ENOENT)
268 fprintf(stderr, "no crontab for %s\n", User);
269 else
270 perror(n);
271 exit(ERROR_EXIT);
272 }
273 poke_daemon(SPOOL_DIR, RELOAD_CRON);
274 }
275
276 static void
check_error(const char * msg)277 check_error(const char *msg) {
278 CheckErrorCount++;
279 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
280 }
281
282 static void
edit_cmd(void)283 edit_cmd(void) {
284 char n[MAX_FNAME], q[MAX_TEMPSTR], *editor;
285 FILE *f;
286 int ch, t, x;
287 struct stat statbuf, xstatbuf;
288 struct timespec mtimespec;
289 struct timeval tv[2];
290 WAIT_T waiter;
291 PID_T pid, xpid;
292
293 log_it(RealUser, Pid, "BEGIN EDIT", User);
294 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
295 fprintf(stderr, "path too long\n");
296 exit(ERROR_EXIT);
297 }
298 if (!(f = fopen(n, "r"))) {
299 if (errno != ENOENT) {
300 perror(n);
301 exit(ERROR_EXIT);
302 }
303 fprintf(stderr, "no crontab for %s - using an empty one\n",
304 User);
305 if (!(f = fopen(_PATH_DEVNULL, "r"))) {
306 perror(_PATH_DEVNULL);
307 exit(ERROR_EXIT);
308 }
309 }
310
311 if (fstat(fileno(f), &statbuf) < 0) {
312 perror("fstat");
313 goto fatal;
314 }
315 memcpy(&mtimespec, &statbuf.st_mtimespec, sizeof(mtimespec));
316 TIMESPEC_TO_TIMEVAL(&tv[0], &statbuf.st_atimespec);
317 TIMESPEC_TO_TIMEVAL(&tv[1], &statbuf.st_mtimespec);
318
319 /* Turn off signals. */
320 (void)signal(SIGHUP, SIG_IGN);
321 (void)signal(SIGINT, SIG_IGN);
322 (void)signal(SIGQUIT, SIG_IGN);
323
324 if (snprintf(Filename, sizeof Filename, "%s/crontab.XXXXXXXXXX",
325 _PATH_TMP) >= sizeof(Filename)) {
326 fprintf(stderr, "path too long\n");
327 goto fatal;
328 }
329 if (-1 == (t = mkstemp(Filename))) {
330 perror(Filename);
331 goto fatal;
332 }
333 if (!(NewCrontab = fdopen(t, "r+"))) {
334 perror("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 x = 0;
343 while (EOF != (ch = get_char(f))) {
344 if ('#' != ch) {
345 putc(ch, NewCrontab);
346 break;
347 }
348 while (EOF != (ch = get_char(f)))
349 if (ch == '\n')
350 break;
351 if (++x >= NHEADER_LINES)
352 break;
353 }
354
355 /* copy the rest of the crontab (if any) to the temp file.
356 */
357 if (EOF != ch)
358 while (EOF != (ch = get_char(f)))
359 putc(ch, NewCrontab);
360 fclose(f);
361 if (fflush(NewCrontab) < OK) {
362 perror(Filename);
363 exit(ERROR_EXIT);
364 }
365 (void)futimes(t, tv);
366 again:
367 rewind(NewCrontab);
368 if (ferror(NewCrontab)) {
369 fprintf(stderr, "%s: error while writing new crontab to %s\n",
370 ProgramName, Filename);
371 fatal:
372 unlink(Filename);
373 exit(ERROR_EXIT);
374 }
375
376 if (((editor = getenv("VISUAL")) == NULL || *editor == '\0') &&
377 ((editor = getenv("EDITOR")) == NULL || *editor == '\0')) {
378 editor = EDITOR;
379 }
380
381 /* we still have the file open. editors will generally rewrite the
382 * original file rather than renaming/unlinking it and starting a
383 * new one; even backup files are supposed to be made by copying
384 * rather than by renaming. if some editor does not support this,
385 * then don't use it. the security problems are more severe if we
386 * close and reopen the file around the edit.
387 */
388
389 switch (pid = fork()) {
390 case -1:
391 perror("fork");
392 goto fatal;
393 case 0:
394 /* child */
395 if (setgid(MY_GID(pw)) < 0) {
396 perror("setgid(getgid())");
397 exit(ERROR_EXIT);
398 }
399 if (chdir(_PATH_TMP) < 0) {
400 perror(_PATH_TMP);
401 exit(ERROR_EXIT);
402 }
403 if (snprintf(q, sizeof q, "%s %s", editor, Filename) >= sizeof(q)) {
404 fprintf(stderr, "%s: editor command line too long\n",
405 ProgramName);
406 exit(ERROR_EXIT);
407 }
408 execlp(_PATH_BSHELL, _PATH_BSHELL, "-c", q, (char *)NULL);
409 perror(editor);
410 exit(ERROR_EXIT);
411 /*NOTREACHED*/
412 default:
413 /* parent */
414 break;
415 }
416
417 /* parent */
418 for (;;) {
419 xpid = waitpid(pid, &waiter, WUNTRACED);
420 if (xpid == -1) {
421 if (errno != EINTR)
422 fprintf(stderr, "%s: waitpid() failed waiting for PID %ld from \"%s\": %s\n",
423 ProgramName, (long)pid, editor, strerror(errno));
424 } else if (xpid != pid) {
425 fprintf(stderr, "%s: wrong PID (%ld != %ld) from \"%s\"\n",
426 ProgramName, (long)xpid, (long)pid, editor);
427 goto fatal;
428 } else if (WIFSTOPPED(waiter)) {
429 kill(getpid(), WSTOPSIG(waiter));
430 } else if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) {
431 fprintf(stderr, "%s: \"%s\" exited with status %d\n",
432 ProgramName, editor, WEXITSTATUS(waiter));
433 goto fatal;
434 } else if (WIFSIGNALED(waiter)) {
435 fprintf(stderr,
436 "%s: \"%s\" killed; signal %d (%score dumped)\n",
437 ProgramName, editor, WTERMSIG(waiter),
438 WCOREDUMP(waiter) ?"" :"no ");
439 goto fatal;
440 } else
441 break;
442 }
443 (void)signal(SIGHUP, SIG_DFL);
444 (void)signal(SIGINT, SIG_DFL);
445 (void)signal(SIGQUIT, SIG_DFL);
446 if (fstat(t, &statbuf) < 0) {
447 perror("fstat");
448 goto fatal;
449 }
450 if (timespeccmp(&mtimespec, &statbuf.st_mtimespec, -) == 0) {
451 if (lstat(Filename, &xstatbuf) == 0 &&
452 statbuf.st_ino != xstatbuf.st_ino) {
453 fprintf(stderr, "%s: crontab temp file moved, editor "
454 "may create backup files improperly\n", ProgramName);
455 }
456 fprintf(stderr, "%s: no changes made to crontab\n",
457 ProgramName);
458 goto remove;
459 }
460 fprintf(stderr, "%s: installing new crontab\n", ProgramName);
461 switch (replace_cmd()) {
462 case 0:
463 break;
464 case -1:
465 for (;;) {
466 printf("Do you want to retry the same edit? ");
467 fflush(stdout);
468 q[0] = '\0';
469 if (fgets(q, sizeof q, stdin) == NULL) {
470 putchar('\n');
471 goto abandon;
472 }
473 switch (q[0]) {
474 case 'y':
475 case 'Y':
476 goto again;
477 case 'n':
478 case 'N':
479 goto abandon;
480 default:
481 fprintf(stderr, "Enter Y or N\n");
482 }
483 }
484 /*NOTREACHED*/
485 case -2:
486 abandon:
487 fprintf(stderr, "%s: edits left in %s\n",
488 ProgramName, Filename);
489 goto done;
490 default:
491 fprintf(stderr, "%s: panic: bad switch() in replace_cmd()\n",
492 ProgramName);
493 goto fatal;
494 }
495 remove:
496 unlink(Filename);
497 done:
498 log_it(RealUser, Pid, "END EDIT", User);
499 }
500
501 /* returns 0 on success
502 * -1 on syntax error
503 * -2 on install error
504 */
505 static int
replace_cmd(void)506 replace_cmd(void) {
507 char n[MAX_FNAME], envstr[MAX_ENVSTR];
508 FILE *tmp;
509 int ch, eof, fd;
510 int error = 0;
511 entry *e;
512 time_t now = time(NULL);
513 char **envp = env_init();
514
515 if (envp == NULL) {
516 fprintf(stderr, "%s: Cannot allocate memory.\n", ProgramName);
517 return (-2);
518 }
519 if (snprintf(TempFilename, sizeof TempFilename, "%s/tmp.XXXXXXXXX",
520 SPOOL_DIR) >= sizeof(TempFilename)) {
521 TempFilename[0] = '\0';
522 fprintf(stderr, "path too long\n");
523 return (-2);
524 }
525 if ((fd = mkstemp(TempFilename)) == -1 || !(tmp = fdopen(fd, "w+"))) {
526 perror(TempFilename);
527 if (fd != -1) {
528 close(fd);
529 unlink(TempFilename);
530 }
531 TempFilename[0] = '\0';
532 return (-2);
533 }
534
535 (void) signal(SIGHUP, die);
536 (void) signal(SIGINT, die);
537 (void) signal(SIGQUIT, die);
538
539 /* write a signature at the top of the file.
540 *
541 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
542 */
543 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
544 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
545 fprintf(tmp, "# (Cron version %s -- %s)\n", CRON_VERSION,
546 "$MirOS: src/usr.sbin/cron/crontab.c,v 1.4 2007/07/05 23:09:47 tg Exp $");
547
548 /* copy the crontab to the tmp
549 */
550 rewind(NewCrontab);
551 Set_LineNum(1)
552 while (EOF != (ch = get_char(NewCrontab)))
553 putc(ch, tmp);
554 ftruncate(fileno(tmp), ftello(tmp)); /* XXX redundant with "w+"? */
555 fflush(tmp); rewind(tmp);
556
557 if (ferror(tmp)) {
558 fprintf(stderr, "%s: error while writing new crontab to %s\n",
559 ProgramName, TempFilename);
560 fclose(tmp);
561 error = -2;
562 goto done;
563 }
564
565 /* check the syntax of the file being installed.
566 */
567
568 /* BUG: was reporting errors after the EOF if there were any errors
569 * in the file proper -- kludged it by stopping after first error.
570 * vix 31mar87
571 */
572 Set_LineNum(1 - NHEADER_LINES)
573 CheckErrorCount = 0; eof = FALSE;
574 while (!CheckErrorCount && !eof) {
575 switch (load_env(envstr, tmp)) {
576 case ERR:
577 /* check for data before the EOF */
578 if (envstr[0] != '\0') {
579 Set_LineNum(LineNumber + 1);
580 check_error("premature EOF");
581 }
582 eof = TRUE;
583 break;
584 case FALSE:
585 e = load_entry(tmp, check_error, pw, envp);
586 if (e)
587 free(e);
588 break;
589 case TRUE:
590 break;
591 }
592 }
593
594 if (CheckErrorCount != 0) {
595 fprintf(stderr, "errors in crontab file, can't install.\n");
596 fclose(tmp);
597 error = -1;
598 goto done;
599 }
600
601 #ifdef HAVE_FCHOWN
602 if (fchown(fileno(tmp), pw->pw_uid, -1) < OK) {
603 perror("fchown");
604 fclose(tmp);
605 error = -2;
606 goto done;
607 }
608 #else
609 if (chown(TempFilename, pw->pw_uid, -1) < OK) {
610 perror("chown");
611 fclose(tmp);
612 error = -2;
613 goto done;
614 }
615 #endif
616
617 if (fclose(tmp) == EOF) {
618 perror("fclose");
619 error = -2;
620 goto done;
621 }
622
623 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
624 fprintf(stderr, "path too long\n");
625 error = -2;
626 goto done;
627 }
628 if (rename(TempFilename, n)) {
629 fprintf(stderr, "%s: error renaming %s to %s\n",
630 ProgramName, TempFilename, n);
631 perror("rename");
632 error = -2;
633 goto done;
634 }
635 TempFilename[0] = '\0';
636 log_it(RealUser, Pid, "REPLACE", User);
637
638 poke_daemon(SPOOL_DIR, RELOAD_CRON);
639
640 done:
641 (void) signal(SIGHUP, SIG_DFL);
642 (void) signal(SIGINT, SIG_DFL);
643 (void) signal(SIGQUIT, SIG_DFL);
644 if (TempFilename[0]) {
645 (void) unlink(TempFilename);
646 TempFilename[0] = '\0';
647 }
648 return (error);
649 }
650
651 static void
die(int x)652 die(int x) {
653 if (TempFilename[0])
654 (void) unlink(TempFilename);
655 _exit(ERROR_EXIT);
656 }
657