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