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