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