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