1/* vi: set sw=4 ts=4: */
2/*
3 * CRONTAB
4 *
5 * usually setuid root, -c option only works if getuid() == geteuid()
6 *
7 * Copyright 1994 Matthew Dillon (dillon@apollo.west.oic.com)
8 * Vladimir Oleynik <dzo@simtreas.ru> (C) 2002
9 *
10 * Licensed under the GPL v2 or later, see the file LICENSE in this tarball.
11 */
12
13#include "libbb.h"
14
15#ifndef CRONTABS
16#define CRONTABS        "/var/spool/cron/crontabs"
17#endif
18#ifndef TMPDIR
19#define TMPDIR          "/var/spool/cron"
20#endif
21#ifndef CRONUPDATE
22#define CRONUPDATE      "cron.update"
23#endif
24#ifndef PATH_VI
25#define PATH_VI         "/bin/vi"   /* location of vi */
26#endif
27
28static const char *CDir = CRONTABS;
29
30static void EditFile(const char *user, const char *file);
31static int GetReplaceStream(const char *user, const char *file);
32static int ChangeUser(const char *user, short dochdir);
33
34int crontab_main(int ac, char **av);
35int crontab_main(int ac, char **av)
36{
37	enum { NONE, EDIT, LIST, REPLACE, DELETE } option = NONE;
38	const struct passwd *pas;
39	const char *repFile = NULL;
40	int repFd = 0;
41	int i;
42	char caller[256];           /* user that ran program */
43	char buf[1024];
44	int UserId;
45
46	UserId = getuid();
47	pas = getpwuid(UserId);
48	if (pas == NULL)
49		bb_perror_msg_and_die("getpwuid");
50
51	safe_strncpy(caller, pas->pw_name, sizeof(caller));
52
53	i = 1;
54	if (ac > 1) {
55		if (LONE_DASH(av[1])) {
56			option = REPLACE;
57			++i;
58		} else if (av[1][0] != '-') {
59			option = REPLACE;
60			++i;
61			repFile = av[1];
62		}
63	}
64
65	for (; i < ac; ++i) {
66		char *ptr = av[i];
67
68		if (*ptr != '-')
69			break;
70		ptr += 2;
71
72		switch (ptr[-1]) {
73		case 'l':
74			if (ptr[-1] == 'l')
75				option = LIST;
76			/* fall through */
77		case 'e':
78			if (ptr[-1] == 'e')
79				option = EDIT;
80			/* fall through */
81		case 'd':
82			if (ptr[-1] == 'd')
83				option = DELETE;
84			/* fall through */
85		case 'u':
86			if (i + 1 < ac && av[i+1][0] != '-') {
87				++i;
88				if (getuid() == geteuid()) {
89					pas = getpwnam(av[i]);
90					if (pas) {
91						UserId = pas->pw_uid;
92					} else {
93						bb_error_msg_and_die("user %s unknown", av[i]);
94					}
95				} else {
96					bb_error_msg_and_die("only the superuser may specify a user");
97				}
98			}
99			break;
100		case 'c':
101			if (getuid() == geteuid()) {
102				CDir = (*ptr) ? ptr : av[++i];
103			} else {
104				bb_error_msg_and_die("-c option: superuser only");
105			}
106			break;
107		default:
108			i = ac;
109			break;
110		}
111	}
112	if (i != ac || option == NONE)
113		bb_show_usage();
114
115	/*
116	 * Get password entry
117	 */
118
119	pas = getpwuid(UserId);
120	if (pas == NULL)
121		bb_perror_msg_and_die("getpwuid");
122
123	/*
124	 * If there is a replacement file, obtain a secure descriptor to it.
125	 */
126
127	if (repFile) {
128		repFd = GetReplaceStream(caller, repFile);
129		if (repFd < 0)
130			bb_error_msg_and_die("cannot read replacement file");
131	}
132
133	/*
134	 * Change directory to our crontab directory
135	 */
136
137	xchdir(CDir);
138
139	/*
140	 * Handle options as appropriate
141	 */
142
143	switch (option) {
144	case LIST:
145		{
146			FILE *fi;
147
148			fi = fopen(pas->pw_name, "r");
149			if (fi) {
150				while (fgets(buf, sizeof(buf), fi) != NULL)
151					fputs(buf, stdout);
152				fclose(fi);
153			} else {
154				bb_error_msg("no crontab for %s", pas->pw_name);
155			}
156		}
157		break;
158	case EDIT:
159		{
160			FILE *fi;
161			int fd;
162			int n;
163			char tmp[128];
164
165			snprintf(tmp, sizeof(tmp), TMPDIR "/crontab.%d", getpid());
166			fd = xopen3(tmp, O_RDWR|O_CREAT|O_TRUNC|O_EXCL, 0600);
167/* race, use fchown */
168			chown(tmp, getuid(), getgid());
169			fi = fopen(pas->pw_name, "r");
170			if (fi) {
171				while ((n = fread(buf, 1, sizeof(buf), fi)) > 0)
172					full_write(fd, buf, n);
173			}
174			EditFile(caller, tmp);
175			remove(tmp);
176			lseek(fd, 0L, SEEK_SET);
177			repFd = fd;
178		}
179		option = REPLACE;
180		/* fall through */
181	case REPLACE:
182		{
183/* same here */
184			char path[1024];
185			int fd;
186			int n;
187
188			snprintf(path, sizeof(path), "%s.new", pas->pw_name);
189			fd = open(path, O_CREAT|O_TRUNC|O_APPEND|O_WRONLY, 0600);
190			if (fd >= 0) {
191				while ((n = read(repFd, buf, sizeof(buf))) > 0) {
192					full_write(fd, buf, n);
193				}
194				close(fd);
195				rename(path, pas->pw_name);
196			} else {
197				bb_error_msg("cannot create %s/%s", CDir, path);
198			}
199			close(repFd);
200		}
201		break;
202	case DELETE:
203		remove(pas->pw_name);
204		break;
205	case NONE:
206	default:
207		break;
208	}
209
210	/*
211	 *  Bump notification file.  Handle window where crond picks file up
212	 *  before we can write our entry out.
213	 */
214
215	if (option == REPLACE || option == DELETE) {
216		FILE *fo;
217		struct stat st;
218
219		while ((fo = fopen(CRONUPDATE, "a"))) {
220			fprintf(fo, "%s\n", pas->pw_name);
221			fflush(fo);
222			if (fstat(fileno(fo), &st) != 0 || st.st_nlink != 0) {
223				fclose(fo);
224				break;
225			}
226			fclose(fo);
227			/* loop */
228		}
229		if (fo == NULL) {
230			bb_error_msg("cannot append to %s/%s", CDir, CRONUPDATE);
231		}
232	}
233	return 0;
234}
235
236static int GetReplaceStream(const char *user, const char *file)
237{
238	int filedes[2];
239	int pid;
240	int fd;
241	int n;
242	char buf[1024];
243
244	if (pipe(filedes) < 0) {
245		perror("pipe");
246		return -1;
247	}
248	pid = fork();
249	if (pid < 0) {
250		perror("fork");
251		return -1;
252	}
253	if (pid > 0) {
254		/*
255		 * PARENT
256		 */
257
258		close(filedes[1]);
259		if (read(filedes[0], buf, 1) != 1) {
260			close(filedes[0]);
261			filedes[0] = -1;
262		}
263		return filedes[0];
264	}
265
266	/*
267	 * CHILD
268	 */
269
270	close(filedes[0]);
271
272	if (ChangeUser(user, 0) < 0)
273		exit(0);
274
275	xfunc_error_retval = 0;
276	fd = xopen(file, O_RDONLY);
277	buf[0] = 0;
278	write(filedes[1], buf, 1);
279	while ((n = read(fd, buf, sizeof(buf))) > 0) {
280		write(filedes[1], buf, n);
281	}
282	exit(0);
283}
284
285static void EditFile(const char *user, const char *file)
286{
287	int pid = fork();
288
289	if (pid == 0) {
290		/*
291		 * CHILD - change user and run editor
292		 */
293		const char *ptr;
294
295		if (ChangeUser(user, 1) < 0)
296			exit(0);
297		ptr = getenv("VISUAL");
298		if (ptr == NULL)
299			ptr = getenv("EDITOR");
300		if (ptr == NULL)
301			ptr = PATH_VI;
302
303		ptr = xasprintf("%s %s", ptr, file);
304		execl(DEFAULT_SHELL, DEFAULT_SHELL, "-c", ptr, NULL);
305		bb_perror_msg_and_die("exec");
306	}
307	if (pid < 0) {
308		/*
309		 * PARENT - failure
310		 */
311		bb_perror_msg_and_die("fork");
312	}
313	wait4(pid, NULL, 0, NULL);
314}
315
316static int ChangeUser(const char *user, short dochdir)
317{
318	struct passwd *pas;
319
320	/*
321	 * Obtain password entry and change privileges
322	 */
323
324	pas = getpwnam(user);
325	if (pas == NULL) {
326		bb_perror_msg_and_die("failed to get uid for %s", user);
327	}
328	setenv("USER", pas->pw_name, 1);
329	setenv("HOME", pas->pw_dir, 1);
330	setenv("SHELL", DEFAULT_SHELL, 1);
331
332	/*
333	 * Change running state to the user in question
334	 */
335	change_identity(pas);
336
337	if (dochdir) {
338		if (chdir(pas->pw_dir) < 0) {
339			bb_perror_msg("chdir(%s) by %s failed", pas->pw_dir, user);
340			xchdir(TMPDIR);
341		}
342	}
343	return pas->pw_uid;
344}
345