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