crontab.c revision 2311
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#if !defined(lint) && !defined(LINT) 19static char rcsid[] = "$Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp $"; 20#endif 21 22/* crontab - install and manage per-user crontab files 23 * vix 02may87 [RCS has the rest of the log] 24 * vix 26jan87 [original] 25 */ 26 27 28#define MAIN_PROGRAM 29 30 31#include "cron.h" 32#include <errno.h> 33#include <fcntl.h> 34#include <sys/file.h> 35#include <sys/stat.h> 36#ifdef USE_UTIMES 37# include <sys/time.h> 38#else 39# include <time.h> 40# include <utime.h> 41#endif 42#if defined(POSIX) 43# include <locale.h> 44#endif 45 46 47#define NHEADER_LINES 3 48 49 50enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace }; 51 52#if DEBUGGING 53static char *Options[] = { "???", "list", "delete", "edit", "replace" }; 54#endif 55 56 57static PID_T Pid; 58static char User[MAX_UNAME], RealUser[MAX_UNAME]; 59static char Filename[MAX_FNAME]; 60static FILE *NewCrontab; 61static int CheckErrorCount; 62static enum opt_t Option; 63static struct passwd *pw; 64static void list_cmd __P((void)), 65 delete_cmd __P((void)), 66 edit_cmd __P((void)), 67 poke_daemon __P((void)), 68 check_error __P((char *)), 69 parse_args __P((int c, char *v[])); 70static int replace_cmd __P((void)); 71 72 73static void 74usage(msg) 75 char *msg; 76{ 77 fprintf(stderr, "%s: usage error: %s\n", ProgramName, msg); 78 fprintf(stderr, "usage:\t%s [-u user] file\n", ProgramName); 79 fprintf(stderr, "\t%s [-u user] { -e | -l | -r }\n", ProgramName); 80 fprintf(stderr, "\t\t(default operation is replace, per 1003.2)\n"); 81 fprintf(stderr, "\t-e\t(edit user's crontab)\n"); 82 fprintf(stderr, "\t-l\t(list user's crontab)\n"); 83 fprintf(stderr, "\t-r\t(delete user's crontab)\n"); 84 exit(ERROR_EXIT); 85} 86 87 88int 89main(argc, argv) 90 int argc; 91 char *argv[]; 92{ 93 int exitstatus; 94 95 Pid = getpid(); 96 ProgramName = argv[0]; 97 98#if defined(POSIX) 99 setlocale(LC_ALL, ""); 100#endif 101 102#if defined(BSD) 103 setlinebuf(stderr); 104#endif 105 parse_args(argc, argv); /* sets many globals, opens a file */ 106 set_cron_uid(); 107 set_cron_cwd(); 108 if (!allowed(User)) { 109 fprintf(stderr, 110 "You (%s) are not allowed to use this program (%s)\n", 111 User, ProgramName); 112 fprintf(stderr, "See crontab(1) for more information\n"); 113 log_it(RealUser, Pid, "AUTH", "crontab command not allowed"); 114 exit(ERROR_EXIT); 115 } 116 exitstatus = OK_EXIT; 117 switch (Option) { 118 case opt_list: list_cmd(); 119 break; 120 case opt_delete: delete_cmd(); 121 break; 122 case opt_edit: edit_cmd(); 123 break; 124 case opt_replace: if (replace_cmd() < 0) 125 exitstatus = ERROR_EXIT; 126 break; 127 } 128 exit(0); 129 /*NOTREACHED*/ 130} 131 132 133static void 134parse_args(argc, argv) 135 int argc; 136 char *argv[]; 137{ 138 int argch; 139 140 if (!(pw = getpwuid(getuid()))) { 141 fprintf(stderr, "%s: your UID isn't in the passwd file.\n", 142 ProgramName); 143 fprintf(stderr, "bailing out.\n"); 144 exit(ERROR_EXIT); 145 } 146 strcpy(User, pw->pw_name); 147 strcpy(RealUser, User); 148 Filename[0] = '\0'; 149 Option = opt_unknown; 150 while (EOF != (argch = getopt(argc, argv, "u:lerx:"))) { 151 switch (argch) { 152 case 'x': 153 if (!set_debug_flags(optarg)) 154 usage("bad debug option"); 155 break; 156 case 'u': 157 if (getuid() != ROOT_UID) 158 { 159 fprintf(stderr, 160 "must be privileged to use -u\n"); 161 exit(ERROR_EXIT); 162 } 163 if (!(pw = getpwnam(optarg))) 164 { 165 fprintf(stderr, "%s: user `%s' unknown\n", 166 ProgramName, optarg); 167 exit(ERROR_EXIT); 168 } 169 (void) strcpy(User, optarg); 170 break; 171 case 'l': 172 if (Option != opt_unknown) 173 usage("only one operation permitted"); 174 Option = opt_list; 175 break; 176 case 'r': 177 if (Option != opt_unknown) 178 usage("only one operation permitted"); 179 Option = opt_delete; 180 break; 181 case 'e': 182 if (Option != opt_unknown) 183 usage("only one operation permitted"); 184 Option = opt_edit; 185 break; 186 default: 187 usage("unrecognized option"); 188 } 189 } 190 191 endpwent(); 192 193 if (Option != opt_unknown) { 194 if (argv[optind] != NULL) { 195 usage("no arguments permitted after this option"); 196 } 197 } else { 198 if (argv[optind] != NULL) { 199 Option = opt_replace; 200 (void) strcpy (Filename, 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 242static void 243list_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 267static void 268delete_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 284static void 285check_error(msg) 286 char *msg; 287{ 288 CheckErrorCount++; 289 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg); 290} 291 292 293static void 294edit_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, "r+"))) { 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 (fflush(NewCrontab) < OK) { 362 perror(Filename); 363 exit(ERROR_EXIT); 364 } 365 again: 366 rewind(NewCrontab); 367 if (ferror(NewCrontab)) { 368 fprintf(stderr, "%s: error while writing new crontab to %s\n", 369 ProgramName, Filename); 370 fatal: unlink(Filename); 371 exit(ERROR_EXIT); 372 } 373 if (fstat(t, &statbuf) < 0) { 374 perror("fstat"); 375 goto fatal; 376 } 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 perror("fork"); 396 goto fatal; 397 case 0: 398 /* child */ 399 if (setuid(getuid()) < 0) { 400 perror("setuid(getuid())"); 401 exit(ERROR_EXIT); 402 } 403 if (chdir("/tmp") < 0) { 404 perror("chdir(/tmp)"); 405 exit(ERROR_EXIT); 406 } 407 if (strlen(editor) + strlen(Filename) + 2 >= MAX_TEMPSTR) { 408 fprintf(stderr, "%s: editor or filename too long\n", 409 ProgramName); 410 exit(ERROR_EXIT); 411 } 412 sprintf(q, "%s %s", editor, Filename); 413 execlp(_PATH_BSHELL, _PATH_BSHELL, "-c", q, NULL); 414 perror(editor); 415 exit(ERROR_EXIT); 416 /*NOTREACHED*/ 417 default: 418 /* parent */ 419 break; 420 } 421 422 /* parent */ 423 xpid = wait(&waiter); 424 if (xpid != pid) { 425 fprintf(stderr, "%s: wrong PID (%d != %d) from \"%s\"\n", 426 ProgramName, xpid, pid, editor); 427 goto fatal; 428 } 429 if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) { 430 fprintf(stderr, "%s: \"%s\" exited with status %d\n", 431 ProgramName, editor, WEXITSTATUS(waiter)); 432 goto fatal; 433 } 434 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 } 441 if (fstat(t, &statbuf) < 0) { 442 perror("fstat"); 443 goto fatal; 444 } 445 if (mtime == statbuf.st_mtime) { 446 fprintf(stderr, "%s: no changes made to crontab\n", 447 ProgramName); 448 goto remove; 449 } 450 fprintf(stderr, "%s: installing new crontab\n", ProgramName); 451 switch (replace_cmd()) { 452 case 0: 453 break; 454 case -1: 455 for (;;) { 456 printf("Do you want to retry the same edit? "); 457 fflush(stdout); 458 q[0] = '\0'; 459 (void) fgets(q, sizeof q, stdin); 460 switch (islower(q[0]) ? q[0] : tolower(q[0])) { 461 case 'y': 462 goto again; 463 case 'n': 464 goto abandon; 465 default: 466 fprintf(stderr, "Enter Y or N\n"); 467 } 468 } 469 /*NOTREACHED*/ 470 case -2: 471 abandon: 472 fprintf(stderr, "%s: edits left in %s\n", 473 ProgramName, Filename); 474 goto done; 475 default: 476 fprintf(stderr, "%s: panic: bad switch() in replace_cmd()\n"); 477 goto fatal; 478 } 479 remove: 480 unlink(Filename); 481 done: 482 log_it(RealUser, Pid, "END EDIT", User); 483} 484 485 486/* returns 0 on success 487 * -1 on syntax error 488 * -2 on install error 489 */ 490static int 491replace_cmd() { 492 char n[MAX_FNAME], envstr[MAX_ENVSTR], tn[MAX_FNAME]; 493 FILE *tmp; 494 int ch, eof; 495 entry *e; 496 time_t now = time(NULL); 497 char **envp = env_init(); 498 499 (void) sprintf(n, "tmp.%d", Pid); 500 (void) sprintf(tn, CRON_TAB(n)); 501 if (!(tmp = fopen(tn, "w+"))) { 502 perror(tn); 503 return (-2); 504 } 505 506 /* write a signature at the top of the file. 507 * 508 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code. 509 */ 510 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n"); 511 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now)); 512 fprintf(tmp, "# (Cron version -- %s)\n", rcsid); 513 514 /* copy the crontab to the tmp 515 */ 516 rewind(NewCrontab); 517 Set_LineNum(1) 518 while (EOF != (ch = get_char(NewCrontab))) 519 putc(ch, tmp); 520 ftruncate(fileno(tmp), ftell(tmp)); 521 fflush(tmp); rewind(tmp); 522 523 if (ferror(tmp)) { 524 fprintf(stderr, "%s: error while writing new crontab to %s\n", 525 ProgramName, 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 fprintf(stderr, "errors in crontab file, can't install.\n"); 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 perror("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 perror("chown"); 578 fclose(tmp); unlink(tn); 579 return (-2); 580 } 581 582 if (fclose(tmp) == EOF) { 583 perror("fclose"); 584 unlink(tn); 585 return (-2); 586 } 587 588 (void) sprintf(n, CRON_TAB(User)); 589 if (rename(tn, n)) { 590 fprintf(stderr, "%s: error renaming %s to %s\n", 591 ProgramName, tn, n); 592 perror("rename"); 593 unlink(tn); 594 return (-2); 595 } 596 log_it(RealUser, Pid, "REPLACE", User); 597 598 poke_daemon(); 599 600 return (0); 601} 602 603 604static void 605poke_daemon() { 606#ifdef USE_UTIMES 607 struct timeval tvs[2]; 608 struct timezone tz; 609 610 (void) gettimeofday(&tvs[0], &tz); 611 tvs[1] = tvs[0]; 612 if (utimes(SPOOL_DIR, tvs) < OK) { 613 fprintf(stderr, "crontab: can't update mtime on spooldir\n"); 614 perror(SPOOL_DIR); 615 return; 616 } 617#else 618 if (utime(SPOOL_DIR, NULL) < OK) { 619 fprintf(stderr, "crontab: can't update mtime on spooldir\n"); 620 perror(SPOOL_DIR); 621 return; 622 } 623#endif /*USE_UTIMES*/ 624} 625