*: more readable handling of pipe fds. No code changes.
[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 "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
28 static void change_user(const struct passwd *pas)
29 {
30         setenv("USER", pas->pw_name, 1);
31         setenv("HOME", pas->pw_dir, 1);
32         setenv("SHELL", DEFAULT_SHELL, 1);
33
34         /* initgroups, setgid, setuid */
35         change_identity(pas);
36
37         if (chdir(pas->pw_dir) < 0) {
38                 bb_perror_msg("chdir(%s) by %s failed",
39                                 pas->pw_dir, pas->pw_name);
40                 xchdir(TMPDIR);
41         }
42 }
43
44 static void edit_file(const struct passwd *pas, const char *file)
45 {
46         const char *ptr;
47         int pid = vfork();
48
49         if (pid < 0) /* failure */
50                 bb_perror_msg_and_die("vfork");
51         if (pid) { /* parent */
52                 wait4pid(pid);
53                 return;
54         }
55
56         /* CHILD - change user and run editor */
57         change_user(pas);
58         ptr = getenv("VISUAL");
59         if (!ptr) {
60                 ptr = getenv("EDITOR");
61                 if (!ptr)
62                         ptr = PATH_VI;
63         }
64
65         /* TODO: clean up the environment!!! */
66         /* not execlp - we won't use PATH */
67         execl(ptr, ptr, file, NULL);
68         bb_perror_msg_and_die("exec %s", ptr);
69 }
70
71 static int open_as_user(const struct passwd *pas, const char *file)
72 {
73         struct fd_pair filedes;
74         pid_t pid;
75         char c;
76
77         xpiped_pair(filedes);
78         pid = vfork();
79         if (pid < 0) /* ERROR */
80                 bb_perror_msg_and_die("vfork");
81         if (pid) { /* PARENT */
82                 int n = safe_read(filedes.rd, &c, 1);
83                 close(filedes.rd);
84                 close(filedes.wr);
85                 if (n > 0) /* child says it can read */
86                         return open(file, O_RDONLY);
87                 return -1;
88         }
89
90         /* CHILD */
91
92         /* initgroups, setgid, setuid */
93         change_identity(pas);
94
95         /* We just try to read one byte. If that works, file is readable
96          * under this user. We signal that by sending one byte to parent. */
97         if (safe_read(xopen(file, O_RDONLY), &c, 1) == 1)
98                 safe_write(filedes.wr, &c, 1); /* "papa, I can read!" */
99         _exit(0);
100 }
101
102 int crontab_main(int argc, char **argv) MAIN_EXTERNALLY_VISIBLE;
103 int crontab_main(int argc, char **argv)
104 {
105         const struct passwd *pas;
106         const char *crontab_dir = CRONTABS;
107         char *tmp_fname;
108         char *new_fname;
109         char *user_name;  /* -u USER */
110         int fd;
111         int opt_ler;
112         uid_t my_uid;
113
114         /* file [opts]     Replace crontab from file
115          * - [opts]        Replace crontab from stdin
116          * -u user         User
117          * -c dir          Crontab directory
118          * -l              List crontab for user
119          * -e              Edit crontab for user
120          * -r              Delete crontab for user
121          * bbox also supports -d == -r, but most other crontab
122          * implementations do not. Deprecated.
123          */
124         enum {
125                 OPT_u = (1 << 0),
126                 OPT_c = (1 << 1),
127                 OPT_l = (1 << 2),
128                 OPT_e = (1 << 3),
129                 OPT_r = (1 << 4),
130                 OPT_ler = OPT_l + OPT_e + OPT_r,
131         };
132
133         my_uid = getuid();
134
135         opt_complementary = "?1:dr"; /* max one argument; -d implies -r */
136         opt_ler = getopt32(argv, "u:c:lerd", &user_name, &crontab_dir);
137         argv += optind;
138
139         if (opt_ler & (OPT_u|OPT_c))
140                 if (my_uid != geteuid())
141                         bb_error_msg_and_die("only root can use -c or -u");
142
143         if (opt_ler & OPT_u) {
144                 pas = getpwnam(user_name);
145                 if (!pas)
146                         bb_error_msg_and_die("user %s is not known", user_name);
147                 my_uid = pas->pw_uid;
148         } else {
149                 pas = getpwuid(my_uid);
150                 if (!pas)
151                         bb_perror_msg_and_die("no user record for UID %u",
152                                         (unsigned)my_uid);
153         }
154
155 #define user_name DONT_USE_ME_BEYOND_THIS_POINT
156 #define my_uid    DONT_USE_ME_BEYOND_THIS_POINT
157
158         /* From now on, keep only -l, -e, -r bits */
159         opt_ler &= OPT_ler;
160         if ((opt_ler - 1) & opt_ler) /* more than one bit set? */
161                 bb_show_usage();
162
163         /* Read replacement file under user's UID/GID/group vector */
164         if (!opt_ler) { /* Replace? */
165                 if (!argv[0])
166                         bb_show_usage();
167                 if (NOT_LONE_DASH(argv[0])) {
168                         fd = open_as_user(pas, argv[0]);
169                         if (fd < 0)
170                                 bb_error_msg_and_die("user %s cannot read %s",
171                                                 pas->pw_name, argv[0]);
172                         xmove_fd(fd, STDIN_FILENO);
173                 }
174         }
175
176         /* cd to our crontab directory */
177         xchdir(crontab_dir);
178
179         tmp_fname = NULL;
180
181         /* Handle requested operation */
182         switch (opt_ler) {
183
184         default: /* case OPT_r: Delete */
185                 remove(pas->pw_name);
186                 break;
187
188         case OPT_l: /* List */
189                 {
190                         char *args[2] = { pas->pw_name, NULL };
191                         return bb_cat(args);
192                         /* list exits,
193                          * the rest go play with cron update file */
194                 }
195
196         case OPT_e: /* Edit */
197                 tmp_fname = xasprintf(TMPDIR "/crontab.%u", (unsigned)getpid());
198                 fd = xopen3(tmp_fname, O_RDWR|O_CREAT|O_TRUNC|O_EXCL, 0600);
199                 xmove_fd(fd, STDIN_FILENO);
200                 fd = open(pas->pw_name, O_RDONLY);
201                 if (fd >= 0) {
202                         bb_copyfd_eof(fd, STDIN_FILENO);
203                         close(fd);
204                 }
205                 fchown(STDIN_FILENO, pas->pw_uid, pas->pw_gid);
206                 edit_file(pas, tmp_fname);
207                 xlseek(STDIN_FILENO, 0, SEEK_SET);
208                 /* fall through */
209
210         case 0: /* Replace (no -l, -e, or -r were given) */
211                 new_fname = xasprintf("%s.new", pas->pw_name);
212                 fd = open(new_fname, O_WRONLY|O_CREAT|O_TRUNC|O_APPEND, 0600);
213                 if (fd >= 0) {
214                         bb_copyfd_eof(STDIN_FILENO, fd);
215                         close(fd);
216                         rename(new_fname, pas->pw_name);
217                 } else {
218                         bb_error_msg("cannot create %s/%s",
219                                         crontab_dir, new_fname);
220                 }
221                 if (tmp_fname)
222                         remove(tmp_fname);
223                 /*free(tmp_fname);*/
224                 /*free(new_fname);*/
225
226         } /* switch */
227
228         /* Bump notification file.  Handle window where crond picks file up
229          * before we can write our entry out.
230          */
231         while ((fd = open(CRONUPDATE, O_WRONLY|O_CREAT|O_APPEND)) >= 0) {
232                 struct stat st;
233
234                 fdprintf(fd, "%s\n", pas->pw_name);
235                 if (fstat(fd, &st) != 0 || st.st_nlink != 0) {
236                         /*close(fd);*/
237                         break;
238                 }
239                 /* st.st_nlink == 0:
240                  * file was deleted, maybe crond missed our notification */
241                 close(fd);
242                 /* loop */
243         }
244         if (fd < 0) {
245                 bb_error_msg("cannot append to %s/%s",
246                                 crontab_dir, CRONUPDATE);
247         }
248         return 0;
249 }