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