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