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