1 /* $OpenBSD: entry.c,v 1.8 2001/02/18 19:48:33 millert Exp $ */ 2 /* 3 * Copyright 1988,1990,1993,1994 by Paul Vixie 4 * All rights reserved 5 */ 6 7 /* 8 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc. 9 * 10 * Permission to use, copy, modify, and distribute this software for any 11 * purpose with or without fee is hereby granted, provided that the above 12 * copyright notice and this permission notice appear in all copies. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS 15 * ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES 16 * OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE 17 * CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL 18 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR 19 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 20 * ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 21 * SOFTWARE. 22 */ 23 24 #if !defined(lint) && !defined(LINT) 25 static char rcsid[] = "$OpenBSD: entry.c,v 1.8 2001/02/18 19:48:33 millert Exp $"; 26 #endif 27 28 /* vix 26jan87 [RCS'd; rest of log is in RCS file] 29 * vix 01jan87 [added line-level error recovery] 30 * vix 31dec86 [added /step to the from-to range, per bob@acornrc] 31 * vix 30dec86 [written] 32 */ 33 34 35 #include "cron.h" 36 37 38 typedef enum ecode { 39 e_none, e_minute, e_hour, e_dom, e_month, e_dow, 40 e_cmd, e_timespec, e_username, e_option 41 } ecode_e; 42 43 static const char *ecodes[] = 44 { 45 "no error", 46 "bad minute", 47 "bad hour", 48 "bad day-of-month", 49 "bad month", 50 "bad day-of-week", 51 "bad command", 52 "bad time specifier", 53 "bad username", 54 "bad option" 55 }; 56 57 static char get_list(bitstr_t *, int, int, const char *[], int, FILE *), 58 get_range(bitstr_t *, int, int, const char *[], int, FILE *), 59 get_number(int *, int, const char *[], int, FILE *); 60 static int set_element(bitstr_t *, int, int, int); 61 62 void 63 free_entry(e) 64 entry *e; 65 { 66 free(e->cmd); 67 env_free(e->envp); 68 free(e); 69 } 70 71 72 /* return NULL if eof or syntax error occurs; 73 * otherwise return a pointer to a new entry. 74 */ 75 entry * 76 load_entry(file, error_func, pw, envp) 77 FILE *file; 78 void (*error_func)(); 79 struct passwd *pw; 80 char **envp; 81 { 82 /* this function reads one crontab entry -- the next -- from a file. 83 * it skips any leading blank lines, ignores comments, and returns 84 * EOF if for any reason the entry can't be read and parsed. 85 * 86 * the entry is also parsed here. 87 * 88 * syntax: 89 * user crontab: 90 * minutes hours doms months dows cmd\n 91 * system crontab (/etc/crontab): 92 * minutes hours doms months dows USERNAME cmd\n 93 */ 94 95 ecode_e ecode = e_none; 96 entry *e; 97 int ch; 98 char cmd[MAX_COMMAND]; 99 char envstr[MAX_ENVSTR]; 100 char **tenvp; 101 102 Debug(DPARS, ("load_entry()...about to eat comments\n")) 103 104 skip_comments(file); 105 106 ch = get_char(file); 107 if (ch == EOF) 108 return NULL; 109 110 /* ch is now the first useful character of a useful line. 111 * it may be an @special or it may be the first character 112 * of a list of minutes. 113 */ 114 115 e = (entry *) calloc(sizeof(entry), sizeof(char)); 116 117 if (ch == '@') { 118 /* all of these should be flagged and load-limited; i.e., 119 * instead of @hourly meaning "0 * * * *" it should mean 120 * "close to the front of every hour but not 'til the 121 * system load is low". Problems are: how do you know 122 * what "low" means? (save me from /etc/cron.conf!) and: 123 * how to guarantee low variance (how low is low?), which 124 * means how to we run roughly every hour -- seems like 125 * we need to keep a history or let the first hour set 126 * the schedule, which means we aren't load-limited 127 * anymore. too much for my overloaded brain. (vix, jan90) 128 * HINT 129 */ 130 ch = get_string(cmd, MAX_COMMAND, file, " \t\n"); 131 if (!strcmp("reboot", cmd)) { 132 e->flags |= WHEN_REBOOT; 133 } else if (!strcmp("yearly", cmd) || !strcmp("annually", cmd)){ 134 bit_set(e->minute, 0); 135 bit_set(e->hour, 0); 136 bit_set(e->dom, 0); 137 bit_set(e->month, 0); 138 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 139 } else if (!strcmp("monthly", cmd)) { 140 bit_set(e->minute, 0); 141 bit_set(e->hour, 0); 142 bit_set(e->dom, 0); 143 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 144 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 145 } else if (!strcmp("weekly", cmd)) { 146 bit_set(e->minute, 0); 147 bit_set(e->hour, 0); 148 bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); 149 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 150 bit_set(e->dow, 0); 151 } else if (!strcmp("daily", cmd) || !strcmp("midnight", cmd)) { 152 bit_set(e->minute, 0); 153 bit_set(e->hour, 0); 154 bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); 155 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 156 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 157 } else if (!strcmp("hourly", cmd)) { 158 bit_set(e->minute, 0); 159 bit_nset(e->hour, 0, (LAST_HOUR-FIRST_HOUR+1)); 160 bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); 161 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 162 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 163 e->flags |= HR_STAR; 164 } else { 165 ecode = e_timespec; 166 goto eof; 167 } 168 /* Advance past whitespace between shortcut and 169 * username/command. 170 */ 171 Skip_Blanks(ch, file); 172 if (ch == EOF) { 173 ecode = e_cmd; 174 goto eof; 175 } 176 } else { 177 Debug(DPARS, ("load_entry()...about to parse numerics\n")) 178 179 if (ch == '*') 180 e->flags |= MIN_STAR; 181 ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE, 182 PPC_NULL, ch, file); 183 if (ch == EOF) { 184 ecode = e_minute; 185 goto eof; 186 } 187 188 /* hours 189 */ 190 191 if (ch == '*') 192 e->flags |= HR_STAR; 193 ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR, 194 PPC_NULL, ch, file); 195 if (ch == EOF) { 196 ecode = e_hour; 197 goto eof; 198 } 199 200 /* DOM (days of month) 201 */ 202 203 if (ch == '*') 204 e->flags |= DOM_STAR; 205 ch = get_list(e->dom, FIRST_DOM, LAST_DOM, 206 PPC_NULL, ch, file); 207 if (ch == EOF) { 208 ecode = e_dom; 209 goto eof; 210 } 211 212 /* month 213 */ 214 215 ch = get_list(e->month, FIRST_MONTH, LAST_MONTH, 216 MonthNames, ch, file); 217 if (ch == EOF) { 218 ecode = e_month; 219 goto eof; 220 } 221 222 /* DOW (days of week) 223 */ 224 225 if (ch == '*') 226 e->flags |= DOW_STAR; 227 ch = get_list(e->dow, FIRST_DOW, LAST_DOW, 228 DowNames, ch, file); 229 if (ch == EOF) { 230 ecode = e_dow; 231 goto eof; 232 } 233 } 234 235 /* make sundays equivilent */ 236 if (bit_test(e->dow, 0) || bit_test(e->dow, 7)) { 237 bit_set(e->dow, 0); 238 bit_set(e->dow, 7); 239 } 240 241 /* ch is the first character of a command, or a username */ 242 unget_char(ch, file); 243 244 if (!pw) { 245 char *username = cmd; /* temp buffer */ 246 247 Debug(DPARS, ("load_entry()...about to parse username\n")) 248 ch = get_string(username, MAX_COMMAND, file, " \t"); 249 250 Debug(DPARS, ("load_entry()...got %s\n",username)) 251 if (ch == EOF) { 252 ecode = e_cmd; 253 goto eof; 254 } 255 256 pw = getpwnam(username); 257 if (pw == NULL) { 258 ecode = e_username; 259 goto eof; 260 } 261 Debug(DPARS, ("load_entry()...uid %ld, gid %ld\n", 262 (long)e->uid, (long)e->gid)) 263 } else if (ch == '*') { 264 ecode = e_cmd; 265 goto eof; 266 } 267 268 e->uid = pw->pw_uid; 269 e->gid = pw->pw_gid; 270 271 /* copy and fix up environment. some variables are just defaults and 272 * others are overrides. 273 */ 274 if ((e->envp = env_copy(envp)) == NULL) { 275 ecode = e_none; 276 goto eof; 277 } 278 if (!env_get("SHELL", e->envp)) { 279 if (glue_strings(envstr, sizeof envstr, "SHELL", 280 _PATH_BSHELL, '=')) { 281 if ((tenvp = env_set(e->envp, envstr))) { 282 e->envp = tenvp; 283 } else { 284 ecode = e_none; 285 goto eof; 286 } 287 } else 288 log_it("CRON", getpid(), "error", "can't set SHELL"); 289 } 290 if (!env_get("HOME", e->envp)) { 291 if (glue_strings(envstr, sizeof envstr, "HOME", 292 pw->pw_dir, '=')) { 293 if ((tenvp = env_set(e->envp, envstr))) { 294 e->envp = tenvp; 295 } else { 296 ecode = e_none; 297 goto eof; 298 } 299 } else 300 log_it("CRON", getpid(), "error", "can't set HOME"); 301 } 302 #ifdef LOGIN_CAP 303 if (!env_get("PATH", e->envp)) { 304 if (glue_strings(envstr, sizeof envstr, "PATH", 305 _PATH_DEFPATH, '=')) { 306 if ((tenvp = env_set(e->envp, envstr))) { 307 e->envp = tenvp; 308 } else { 309 ecode = e_none; 310 goto eof; 311 } 312 } else 313 log_it("CRON", getpid(), "error", "can't set PATH"); 314 } 315 #endif /* LOGIN_CAP */ 316 if (glue_strings(envstr, sizeof envstr, "LOGNAME", 317 pw->pw_name, '=')) { 318 if ((tenvp = env_set(e->envp, envstr))) { 319 e->envp = tenvp; 320 } else { 321 ecode = e_none; 322 goto eof; 323 } 324 } else 325 log_it("CRON", getpid(), "error", "can't set LOGNAME"); 326 #if defined(BSD) 327 if (glue_strings(envstr, sizeof envstr, "USER", 328 pw->pw_name, '=')) { 329 if ((tenvp = env_set(e->envp, envstr))) { 330 e->envp = tenvp; 331 } else { 332 ecode = e_none; 333 goto eof; 334 } 335 } else 336 log_it("CRON", getpid(), "error", "can't set USER"); 337 #endif 338 339 Debug(DPARS, ("load_entry()...about to parse command\n")) 340 341 /* If the first character of the command is '-' it is a cron option. 342 */ 343 while ((ch = get_char(file)) == '-') { 344 switch (ch = get_char(file)) { 345 case 'q': 346 e->flags |= DONT_LOG; 347 Skip_Nonblanks(ch, file) 348 break; 349 default: 350 ecode = e_option; 351 goto eof; 352 } 353 Skip_Blanks(ch, file) 354 } 355 unget_char(ch, file); 356 357 /* Everything up to the next \n or EOF is part of the command... 358 * too bad we don't know in advance how long it will be, since we 359 * need to malloc a string for it... so, we limit it to MAX_COMMAND. 360 */ 361 ch = get_string(cmd, MAX_COMMAND, file, "\n"); 362 363 /* a file without a \n before the EOF is rude, so we'll complain... 364 */ 365 if (ch == EOF) { 366 ecode = e_cmd; 367 goto eof; 368 } 369 370 /* got the command in the 'cmd' string; save it in *e. 371 */ 372 if ((e->cmd = strdup(cmd)) == NULL) { 373 ecode = e_none; 374 goto eof; 375 } 376 377 Debug(DPARS, ("load_entry()...returning successfully\n")) 378 379 /* success, fini, return pointer to the entry we just created... 380 */ 381 return e; 382 383 eof: 384 if (e->envp) 385 env_free(e->envp); 386 if (e->cmd) 387 free(e->cmd); 388 free(e); 389 if (ecode != e_none && error_func) 390 (*error_func)(ecodes[(int)ecode]); 391 while (ch != EOF && ch != '\n') 392 ch = get_char(file); 393 return NULL; 394 } 395 396 397 static char 398 get_list(bits, low, high, names, ch, file) 399 bitstr_t *bits; /* one bit per flag, default=FALSE */ 400 int low, high; /* bounds, impl. offset for bitstr */ 401 const char *names[]; /* NULL or *[] of names for these elements */ 402 int ch; /* current character being processed */ 403 FILE *file; /* file being read */ 404 { 405 int done; 406 407 /* we know that we point to a non-blank character here; 408 * must do a Skip_Blanks before we exit, so that the 409 * next call (or the code that picks up the cmd) can 410 * assume the same thing. 411 */ 412 413 Debug(DPARS|DEXT, ("get_list()...entered\n")) 414 415 /* list = range {"," range} 416 */ 417 418 /* clear the bit string, since the default is 'off'. 419 */ 420 bit_nclear(bits, 0, (high-low+1)); 421 422 /* process all ranges 423 */ 424 done = FALSE; 425 while (!done) { 426 ch = get_range(bits, low, high, names, ch, file); 427 if (ch == ',') 428 ch = get_char(file); 429 else 430 done = TRUE; 431 } 432 433 /* exiting. skip to some blanks, then skip over the blanks. 434 */ 435 Skip_Nonblanks(ch, file) 436 Skip_Blanks(ch, file) 437 438 Debug(DPARS|DEXT, ("get_list()...exiting w/ %02x\n", ch)) 439 440 return ch; 441 } 442 443 444 static char 445 get_range(bits, low, high, names, ch, file) 446 bitstr_t *bits; /* one bit per flag, default=FALSE */ 447 int low, high; /* bounds, impl. offset for bitstr */ 448 const char *names[]; /* NULL or names of elements */ 449 int ch; /* current character being processed */ 450 FILE *file; /* file being read */ 451 { 452 /* range = number | number "-" number [ "/" number ] 453 */ 454 455 int i, num1, num2, num3; 456 457 Debug(DPARS|DEXT, ("get_range()...entering, exit won't show\n")) 458 459 if (ch == '*') { 460 /* '*' means "first-last" but can still be modified by /step 461 */ 462 num1 = low; 463 num2 = high; 464 ch = get_char(file); 465 if (ch == EOF) 466 return EOF; 467 } else { 468 if (EOF == (ch = get_number(&num1, low, names, ch, file))) 469 return EOF; 470 471 if (ch != '-') { 472 /* not a range, it's a single number. 473 */ 474 if (EOF == set_element(bits, low, high, num1)) 475 return EOF; 476 return ch; 477 } else { 478 /* eat the dash 479 */ 480 ch = get_char(file); 481 if (ch == EOF) 482 return EOF; 483 484 /* get the number following the dash 485 */ 486 ch = get_number(&num2, low, names, ch, file); 487 if (ch == EOF) 488 return EOF; 489 } 490 } 491 492 /* check for step size 493 */ 494 if (ch == '/') { 495 /* eat the slash 496 */ 497 ch = get_char(file); 498 if (ch == EOF) 499 return EOF; 500 501 /* get the step size -- note: we don't pass the 502 * names here, because the number is not an 503 * element id, it's a step size. 'low' is 504 * sent as a 0 since there is no offset either. 505 */ 506 ch = get_number(&num3, 0, PPC_NULL, ch, file); 507 if (ch == EOF) 508 return EOF; 509 } else { 510 /* no step. default==1. 511 */ 512 num3 = 1; 513 } 514 515 /* range. set all elements from num1 to num2, stepping 516 * by num3. (the step is a downward-compatible extension 517 * proposed conceptually by bob@acornrc, syntactically 518 * designed then implmented by paul vixie). 519 */ 520 for (i = num1; i <= num2; i += num3) 521 if (EOF == set_element(bits, low, high, i)) 522 return EOF; 523 524 return ch; 525 } 526 527 528 static char 529 get_number(numptr, low, names, ch, file) 530 int *numptr; /* where does the result go? */ 531 int low; /* offset applied to enum result */ 532 const char *names[]; /* symbolic names, if any, for enums */ 533 int ch; /* current character */ 534 FILE *file; /* source */ 535 { 536 char temp[MAX_TEMPSTR], *pc; 537 int len, i, all_digits; 538 539 /* collect alphanumerics into our fixed-size temp array 540 */ 541 pc = temp; 542 len = 0; 543 all_digits = TRUE; 544 while (isalnum(ch)) { 545 if (++len >= MAX_TEMPSTR) 546 return EOF; 547 548 *pc++ = ch; 549 550 if (!isdigit(ch)) 551 all_digits = FALSE; 552 553 ch = get_char(file); 554 } 555 *pc = '\0'; 556 557 /* try to find the name in the name list 558 */ 559 if (names) { 560 for (i = 0; names[i] != NULL; i++) { 561 Debug(DPARS|DEXT, 562 ("get_num, compare(%s,%s)\n", names[i], temp)) 563 if (!strcasecmp(names[i], temp)) { 564 *numptr = i+low; 565 return ch; 566 } 567 } 568 } 569 570 /* no name list specified, or there is one and our string isn't 571 * in it. either way: if it's all digits, use its magnitude. 572 * otherwise, it's an error. 573 */ 574 if (all_digits) { 575 *numptr = atoi(temp); 576 return ch; 577 } 578 579 return EOF; 580 } 581 582 583 static int 584 set_element(bits, low, high, number) 585 bitstr_t *bits; /* one bit per flag, default=FALSE */ 586 int low; 587 int high; 588 int number; 589 { 590 Debug(DPARS|DEXT, ("set_element(?,%d,%d,%d)\n", low, high, number)) 591 592 if (number < low || number > high) 593 return EOF; 594 595 bit_set(bits, (number-low)); 596 return OK; 597 } 598