1/*	$NetBSD: entry.c,v 1.3 2010/07/10 21:42:29 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
27static 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.3 2010/07/10 21:42:29 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
41typedef	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
46static 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
61static 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
66void
67free_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 */
77entry *
78load_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
404static int
405get_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
448static int
449random_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
459static int
460get_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
579static int
580get_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
626bad:
627	unget_char(ch, file);
628	return (EOF);
629}
630
631static int
632set_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