xref: /dragonfly/usr.sbin/cron/crontab/crontab.c (revision 6bd457ed)
1 /* Copyright 1988,1990,1993,1994 by Paul Vixie
2  * All rights reserved
3  *
4  * Distribute freely, except: don't remove my name from the source or
5  * documentation (don't take credit for my work), mark your changes (don't
6  * get me blamed for your possible bugs), don't alter or remove this
7  * notice.  May be sold if buildable source is provided to buyer.  No
8  * warrantee of any kind, express or implied, is included with this
9  * software; use at your own risk, responsibility for damages (if any) to
10  * anyone resulting from the use of this software rests entirely with the
11  * user.
12  *
13  * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
14  * I'll try to keep a version up to date.  I can be reached as follows:
15  * Paul Vixie          <paul@vix.com>          uunet!decwrl!vixie!paul
16  *
17  * From Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp
18  * $FreeBSD: src/usr.sbin/cron/crontab/crontab.c,v 1.12.2.4 2001/06/16 03:18:37 peter Exp $
19  * $DragonFly: src/usr.sbin/cron/crontab/crontab.c,v 1.5 2004/12/18 22:48:03 swildner Exp $
20  */
21 
22 /* crontab - install and manage per-user crontab files
23  * vix 02may87 [RCS has the rest of the log]
24  * vix 26jan87 [original]
25  */
26 
27 static char version[] = "$DragonFly: src/usr.sbin/cron/crontab/crontab.c,v 1.5 2004/12/18 22:48:03 swildner Exp $";
28 
29 #define	MAIN_PROGRAM
30 
31 #include "cron.h"
32 #include <errno.h>
33 #include <fcntl.h>
34 #include <paths.h>
35 #include <sys/file.h>
36 #include <sys/stat.h>
37 #ifdef USE_UTIMES
38 # include <sys/time.h>
39 #else
40 # include <time.h>
41 # include <utime.h>
42 #endif
43 #if defined(POSIX)
44 # include <locale.h>
45 #endif
46 
47 
48 #define NHEADER_LINES 3
49 
50 
51 enum opt_t	{ opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
52 
53 #if DEBUGGING
54 static char	*Options[] = { "???", "list", "delete", "edit", "replace" };
55 #endif
56 
57 
58 static	PID_T		Pid;
59 static	char		User[MAX_UNAME], RealUser[MAX_UNAME];
60 static	char		Filename[MAX_FNAME];
61 static	FILE		*NewCrontab;
62 static	int		CheckErrorCount;
63 static	enum opt_t	Option;
64 static	struct passwd	*pw;
65 static	void		list_cmd(void),
66 			delete_cmd(void),
67 			edit_cmd(void),
68 			poke_daemon(void),
69 			check_error(char *),
70 			parse_args(int c, char *v[]);
71 static	int		replace_cmd(void);
72 
73 
74 static void
75 usage(char *msg)
76 {
77 	fprintf(stderr, "crontab: usage error: %s\n", msg);
78 	fprintf(stderr, "%s\n%s\n",
79 		"usage: crontab [-u user] file",
80 		"       crontab [-u user] { -e | -l | -r }");
81 	exit(ERROR_EXIT);
82 }
83 
84 
85 int
86 main(int argc, char **argv)
87 {
88 	int	exitstatus;
89 
90 	Pid = getpid();
91 	ProgramName = argv[0];
92 
93 #if defined(POSIX)
94 	setlocale(LC_ALL, "");
95 #endif
96 
97 #if defined(BSD)
98 	setlinebuf(stderr);
99 #endif
100 	parse_args(argc, argv);		/* sets many globals, opens a file */
101 	set_cron_uid();
102 	set_cron_cwd();
103 	if (!allowed(User)) {
104 		warnx("you (%s) are not allowed to use this program", User);
105 		log_it(RealUser, Pid, "AUTH", "crontab command not allowed");
106 		exit(ERROR_EXIT);
107 	}
108 	exitstatus = OK_EXIT;
109 	switch (Option) {
110 	case opt_list:		list_cmd();
111 				break;
112 	case opt_delete:	delete_cmd();
113 				break;
114 	case opt_edit:		edit_cmd();
115 				break;
116 	case opt_replace:	if (replace_cmd() < 0)
117 					exitstatus = ERROR_EXIT;
118 				break;
119 	case opt_unknown:
120 				break;
121 	}
122 	exit(0);
123 	/*NOTREACHED*/
124 }
125 
126 
127 static void
128 parse_args(int argc, char **argv)
129 {
130 	int		argch;
131 
132 	if (!(pw = getpwuid(getuid())))
133 		errx(ERROR_EXIT, "your UID isn't in the passwd file, bailing out");
134 	strncpy(User, pw->pw_name, (sizeof User)-1);
135 	User[(sizeof User)-1] = '\0';
136 	strcpy(RealUser, User);
137 	Filename[0] = '\0';
138 	Option = opt_unknown;
139 	while ((argch = getopt(argc, argv, "u:lerx:")) != -1) {
140 		switch (argch) {
141 		case 'x':
142 			if (!set_debug_flags(optarg))
143 				usage("bad debug option");
144 			break;
145 		case 'u':
146 			if (getuid() != ROOT_UID)
147 				errx(ERROR_EXIT, "must be privileged to use -u");
148 			if (!(pw = getpwnam(optarg)))
149 				errx(ERROR_EXIT, "user `%s' unknown", optarg);
150 			strncpy(User, pw->pw_name, (sizeof User)-1);
151 			User[(sizeof User)-1] = '\0';
152 			break;
153 		case 'l':
154 			if (Option != opt_unknown)
155 				usage("only one operation permitted");
156 			Option = opt_list;
157 			break;
158 		case 'r':
159 			if (Option != opt_unknown)
160 				usage("only one operation permitted");
161 			Option = opt_delete;
162 			break;
163 		case 'e':
164 			if (Option != opt_unknown)
165 				usage("only one operation permitted");
166 			Option = opt_edit;
167 			break;
168 		default:
169 			usage("unrecognized option");
170 		}
171 	}
172 
173 	endpwent();
174 
175 	if (Option != opt_unknown) {
176 		if (argv[optind] != NULL) {
177 			usage("no arguments permitted after this option");
178 		}
179 	} else {
180 		if (argv[optind] != NULL) {
181 			Option = opt_replace;
182 			strncpy (Filename, argv[optind], (sizeof Filename)-1);
183 			Filename[(sizeof Filename)-1] = '\0';
184 
185 		} else {
186 			usage("file name must be specified for replace");
187 		}
188 	}
189 
190 	if (Option == opt_replace) {
191 		/* we have to open the file here because we're going to
192 		 * chdir(2) into /var/cron before we get around to
193 		 * reading the file.
194 		 */
195 		if (!strcmp(Filename, "-")) {
196 			NewCrontab = stdin;
197 		} else {
198 			/* relinquish the setuid status of the binary during
199 			 * the open, lest nonroot users read files they should
200 			 * not be able to read.  we can't use access() here
201 			 * since there's a race condition.  thanks go out to
202 			 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
203 			 * the race.
204 			 */
205 
206 			if (swap_uids() < OK)
207 				err(ERROR_EXIT, "swapping uids");
208 			if (!(NewCrontab = fopen(Filename, "r")))
209 				err(ERROR_EXIT, "%s", Filename);
210 			if (swap_uids() < OK)
211 				err(ERROR_EXIT, "swapping uids back");
212 		}
213 	}
214 
215 	Debug(DMISC, ("user=%s, file=%s, option=%s\n",
216 		      User, Filename, Options[(int)Option]))
217 }
218 
219 
220 static void
221 list_cmd(void)
222 {
223 	char	n[MAX_FNAME];
224 	FILE	*f;
225 	int	ch, x;
226 
227 	log_it(RealUser, Pid, "LIST", User);
228 	sprintf(n, CRON_TAB(User));
229 	if (!(f = fopen(n, "r"))) {
230 		if (errno == ENOENT)
231 			errx(ERROR_EXIT, "no crontab for %s", User);
232 		else
233 			err(ERROR_EXIT, "%s", n);
234 	}
235 
236 	/* file is open. copy to stdout, close.
237 	 */
238 	Set_LineNum(1)
239 
240 	/* ignore the top few comments since we probably put them there.
241 	 */
242 	for (x = 0;  x < NHEADER_LINES;  x++) {
243 		ch = get_char(f);
244 		if (EOF == ch)
245 			break;
246 		if ('#' != ch) {
247 			putchar(ch);
248 			break;
249 		}
250 		while (EOF != (ch = get_char(f)))
251 			if (ch == '\n')
252 				break;
253 		if (EOF == ch)
254 			break;
255 	}
256 
257 	while (EOF != (ch = get_char(f)))
258 		putchar(ch);
259 	fclose(f);
260 }
261 
262 
263 static void
264 delete_cmd(void)
265 {
266 	char	n[MAX_FNAME];
267 	int ch, first;
268 
269 	if (isatty(STDIN_FILENO)) {
270 		fprintf(stderr, "remove crontab for %s? ", User);
271 		first = ch = getchar();
272 		while (ch != '\n' && ch != EOF)
273 			ch = getchar();
274 		if (first != 'y' && first != 'Y')
275 			return;
276 	}
277 
278 	log_it(RealUser, Pid, "DELETE", User);
279 	sprintf(n, CRON_TAB(User));
280 	if (unlink(n)) {
281 		if (errno == ENOENT)
282 			errx(ERROR_EXIT, "no crontab for %s", User);
283 		else
284 			err(ERROR_EXIT, "%s", n);
285 	}
286 	poke_daemon();
287 }
288 
289 
290 static void
291 check_error(char *msg)
292 {
293 	CheckErrorCount++;
294 	fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
295 }
296 
297 
298 static void
299 edit_cmd(void)
300 {
301 	char		n[MAX_FNAME], q[MAX_TEMPSTR], *editor;
302 	FILE		*f;
303 	int		ch, t, x;
304 	struct stat	statbuf, fsbuf;
305 	time_t		mtime;
306 	WAIT_T		waiter;
307 	PID_T		pid, xpid;
308 	mode_t		um;
309 
310 	log_it(RealUser, Pid, "BEGIN EDIT", User);
311 	sprintf(n, CRON_TAB(User));
312 	if (!(f = fopen(n, "r"))) {
313 		if (errno != ENOENT)
314 			err(ERROR_EXIT, "%s", n);
315 		warnx("no crontab for %s - using an empty one", User);
316 		if (!(f = fopen(_PATH_DEVNULL, "r")))
317 			err(ERROR_EXIT, _PATH_DEVNULL);
318 	}
319 
320 	um = umask(077);
321 	sprintf(Filename, "/tmp/crontab.XXXXXXXXXX");
322 	if ((t = mkstemp(Filename)) == -1) {
323 		warn("%s", Filename);
324 		umask(um);
325 		goto fatal;
326 	}
327 	umask(um);
328 #ifdef HAS_FCHOWN
329 	if (fchown(t, getuid(), getgid()) < 0) {
330 #else
331 	if (chown(Filename, getuid(), getgid()) < 0) {
332 #endif
333 		warn("fchown");
334 		goto fatal;
335 	}
336 	if (!(NewCrontab = fdopen(t, "r+"))) {
337 		warn("fdopen");
338 		goto fatal;
339 	}
340 
341 	Set_LineNum(1)
342 
343 	/* ignore the top few comments since we probably put them there.
344 	 */
345 	for (x = 0;  x < NHEADER_LINES;  x++) {
346 		ch = get_char(f);
347 		if (EOF == ch)
348 			break;
349 		if ('#' != ch) {
350 			putc(ch, NewCrontab);
351 			break;
352 		}
353 		while (EOF != (ch = get_char(f)))
354 			if (ch == '\n')
355 				break;
356 		if (EOF == ch)
357 			break;
358 	}
359 
360 	/* copy the rest of the crontab (if any) to the temp file.
361 	 */
362 	if (EOF != ch)
363 		while (EOF != (ch = get_char(f)))
364 			putc(ch, NewCrontab);
365 	fclose(f);
366 	if (fflush(NewCrontab))
367 		err(ERROR_EXIT, "%s", Filename);
368 	if (fstat(t, &fsbuf) < 0) {
369 		warn("unable to fstat temp file");
370 		goto fatal;
371 	}
372  again:
373 	if (stat(Filename, &statbuf) < 0) {
374 		warn("stat");
375  fatal:		unlink(Filename);
376 		exit(ERROR_EXIT);
377 	}
378 	if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
379 		errx(ERROR_EXIT, "temp file must be edited in place");
380 	mtime = statbuf.st_mtime;
381 
382 	if ((!(editor = getenv("VISUAL")))
383 	 && (!(editor = getenv("EDITOR")))
384 	    ) {
385 		editor = EDITOR;
386 	}
387 
388 	/* we still have the file open.  editors will generally rewrite the
389 	 * original file rather than renaming/unlinking it and starting a
390 	 * new one; even backup files are supposed to be made by copying
391 	 * rather than by renaming.  if some editor does not support this,
392 	 * then don't use it.  the security problems are more severe if we
393 	 * close and reopen the file around the edit.
394 	 */
395 
396 	switch (pid = fork()) {
397 	case -1:
398 		warn("fork");
399 		goto fatal;
400 	case 0:
401 		/* child */
402 		if (setuid(getuid()) < 0)
403 			err(ERROR_EXIT, "setuid(getuid())");
404 		if (chdir("/tmp") < 0)
405 			err(ERROR_EXIT, "chdir(/tmp)");
406 		if (strlen(editor) + strlen(Filename) + 2 >= MAX_TEMPSTR)
407 			errx(ERROR_EXIT, "editor or filename too long");
408 		execlp(editor, editor, Filename, NULL);
409 		err(ERROR_EXIT, "%s", editor);
410 		/*NOTREACHED*/
411 	default:
412 		/* parent */
413 		break;
414 	}
415 
416 	/* parent */
417 	{
418 	void (*f[4])();
419 	f[0] = signal(SIGHUP, SIG_IGN);
420 	f[1] = signal(SIGINT, SIG_IGN);
421 	f[2] = signal(SIGTERM, SIG_IGN);
422 	xpid = wait(&waiter);
423 	signal(SIGHUP, f[0]);
424 	signal(SIGINT, f[1]);
425 	signal(SIGTERM, f[2]);
426 	}
427 	if (xpid != pid) {
428 		warnx("wrong PID (%d != %d) from \"%s\"", xpid, pid, editor);
429 		goto fatal;
430 	}
431 	if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) {
432 		warnx("\"%s\" exited with status %d", editor, WEXITSTATUS(waiter));
433 		goto fatal;
434 	}
435 	if (WIFSIGNALED(waiter)) {
436 		warnx("\"%s\" killed; signal %d (%score dumped)",
437 			editor, WTERMSIG(waiter), WCOREDUMP(waiter) ?"" :"no ");
438 		goto fatal;
439 	}
440 	if (stat(Filename, &statbuf) < 0) {
441 		warn("stat");
442 		goto fatal;
443 	}
444 	if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
445 		errx(ERROR_EXIT, "temp file must be edited in place");
446 	if (mtime == statbuf.st_mtime) {
447 		warnx("no changes made to crontab");
448 		goto remove;
449 	}
450 	warnx("installing new crontab");
451 	switch (replace_cmd()) {
452 	case 0:
453 		break;
454 	case -1:
455 		for (;;) {
456 			printf("Do you want to retry the same edit? ");
457 			fflush(stdout);
458 			q[0] = '\0';
459 			fgets(q, sizeof q, stdin);
460 			switch (islower(q[0]) ? q[0] : tolower(q[0])) {
461 			case 'y':
462 				goto again;
463 			case 'n':
464 				goto abandon;
465 			default:
466 				fprintf(stderr, "Enter Y or N\n");
467 			}
468 		}
469 		/*NOTREACHED*/
470 	case -2:
471 	abandon:
472 		warnx("edits left in %s", Filename);
473 		goto done;
474 	default:
475 		warnx("panic: bad switch() in replace_cmd()");
476 		goto fatal;
477 	}
478  remove:
479 	unlink(Filename);
480  done:
481 	log_it(RealUser, Pid, "END EDIT", User);
482 }
483 
484 
485 /* returns	0	on success
486  *		-1	on syntax error
487  *		-2	on install error
488  */
489 static int
490 replace_cmd(void)
491 {
492 	char	n[MAX_FNAME], envstr[MAX_ENVSTR], tn[MAX_FNAME];
493 	FILE	*tmp;
494 	int	ch, eof;
495 	entry	*e;
496 	time_t	now = time(NULL);
497 	char	**envp = env_init();
498 
499 	if (envp == NULL) {
500 		warnx("cannot allocate memory");
501 		return (-2);
502 	}
503 
504 	sprintf(n, "tmp.%d", Pid);
505 	sprintf(tn, CRON_TAB(n));
506 	if (!(tmp = fopen(tn, "w+"))) {
507 		warn("%s", tn);
508 		return (-2);
509 	}
510 
511 	/* write a signature at the top of the file.
512 	 *
513 	 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
514 	 */
515 	fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
516 	fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
517 	fprintf(tmp, "# (Cron version -- %s)\n", version);
518 
519 	/* copy the crontab to the tmp
520 	 */
521 	rewind(NewCrontab);
522 	Set_LineNum(1)
523 	while (EOF != (ch = get_char(NewCrontab)))
524 		putc(ch, tmp);
525 	ftruncate(fileno(tmp), ftell(tmp));
526 	fflush(tmp);  rewind(tmp);
527 
528 	if (ferror(tmp)) {
529 		warnx("error while writing new crontab to %s", tn);
530 		fclose(tmp);  unlink(tn);
531 		return (-2);
532 	}
533 
534 	/* check the syntax of the file being installed.
535 	 */
536 
537 	/* BUG: was reporting errors after the EOF if there were any errors
538 	 * in the file proper -- kludged it by stopping after first error.
539 	 *		vix 31mar87
540 	 */
541 	Set_LineNum(1 - NHEADER_LINES)
542 	CheckErrorCount = 0;  eof = FALSE;
543 	while (!CheckErrorCount && !eof) {
544 		switch (load_env(envstr, tmp)) {
545 		case ERR:
546 			eof = TRUE;
547 			break;
548 		case FALSE:
549 			e = load_entry(tmp, check_error, pw, envp);
550 			if (e)
551 				free(e);
552 			break;
553 		case TRUE:
554 			break;
555 		}
556 	}
557 
558 	if (CheckErrorCount != 0) {
559 		warnx("errors in crontab file, can't install");
560 		fclose(tmp);  unlink(tn);
561 		return (-1);
562 	}
563 
564 #ifdef HAS_FCHOWN
565 	if (fchown(fileno(tmp), ROOT_UID, -1) < OK)
566 #else
567 	if (chown(tn, ROOT_UID, -1) < OK)
568 #endif
569 	{
570 		warn("chown");
571 		fclose(tmp);  unlink(tn);
572 		return (-2);
573 	}
574 
575 #ifdef HAS_FCHMOD
576 	if (fchmod(fileno(tmp), 0600) < OK)
577 #else
578 	if (chmod(tn, 0600) < OK)
579 #endif
580 	{
581 		warn("chown");
582 		fclose(tmp);  unlink(tn);
583 		return (-2);
584 	}
585 
586 	if (fclose(tmp) == EOF) {
587 		warn("fclose");
588 		unlink(tn);
589 		return (-2);
590 	}
591 
592 	sprintf(n, CRON_TAB(User));
593 	if (rename(tn, n)) {
594 		warn("error renaming %s to %s", tn, n);
595 		unlink(tn);
596 		return (-2);
597 	}
598 	log_it(RealUser, Pid, "REPLACE", User);
599 
600 	poke_daemon();
601 
602 	return (0);
603 }
604 
605 
606 static void
607 poke_daemon(void)
608 {
609 #ifdef USE_UTIMES
610 	struct timeval tvs[2];
611 	struct timezone tz;
612 
613 	gettimeofday(&tvs[0], &tz);
614 	tvs[1] = tvs[0];
615 	if (utimes(SPOOL_DIR, tvs) < OK) {
616 		warn("can't update mtime on spooldir %s", SPOOL_DIR);
617 		return;
618 	}
619 #else
620 	if (utime(SPOOL_DIR, NULL) < OK) {
621 		warn("can't update mtime on spooldir %s", SPOOL_DIR);
622 		return;
623 	}
624 #endif /*USE_UTIMES*/
625 }
626