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