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