1 /* crontab.c - files used to schedule the execution of programs.
2  *
3  * Copyright 2014 Ranjan Kumar <ranjankumar.bth@gmail.com>
4  *
5  * http://pubs.opengroup.org/onlinepubs/9699919799/utilities/crontab.html
6 
7 USE_CRONTAB(NEWTOY(crontab, "c:u:elr[!elr]", TOYFLAG_USR|TOYFLAG_BIN|TOYFLAG_STAYROOT))
8 
9 config CRONTAB
10   bool "crontab"
11   default n
12   help
13     usage: crontab [-u user] FILE
14                    [-u user] [-e | -l | -r]
15                    [-c dir]
16 
17     Files used to schedule the execution of programs.
18 
19     -c crontab dir
20     -e edit user's crontab
21     -l list user's crontab
22     -r delete user's crontab
23     -u user
24     FILE Replace crontab by FILE ('-': stdin)
25 */
26 #define FOR_crontab
27 #include "toys.h"
28 
GLOBALS(char * user;char * cdir;)29 GLOBALS(
30   char *user;
31   char *cdir;
32 )
33 
34 static char *omitspace(char *line)
35 {
36   while (*line == ' ' || *line == '\t') line++;
37   return line;
38 }
39 
40 /*
41  * Names can also be used for the 'month' and 'day of week' fields
42  * (First three letters of the particular day or month).
43  */
getindex(char * src,int size)44 static int getindex(char *src, int size)
45 {
46   int i;
47   char days[]={"sun""mon""tue""wed""thu""fri""sat"};
48   char months[]={"jan""feb""mar""apr""may""jun""jul"
49     "aug""sep""oct""nov""dec"};
50   char *field = (size == 12) ? months : days;
51 
52   // strings are not allowed for min, hour and dom fields.
53   if (!(size == 7 || size == 12)) return -1;
54 
55   for (i = 0; field[i]; i += 3) {
56     if (!strncasecmp(src, &field[i], 3))
57       return (i/3);
58   }
59   return -1;
60 }
61 
getval(char * num,long low,long high)62 static long getval(char *num, long low, long high)
63 {
64   long val = strtol(num, &num, 10);
65 
66   if (*num || (val < low) || (val > high)) return -1;
67   return val;
68 }
69 
70 // Validate minute, hour, day of month, month and day of week fields.
validate_component(int min,int max,char * src)71 static int validate_component(int min, int max, char *src)
72 {
73   int skip = 0;
74   char *ptr;
75 
76   if (!src) return 1;
77   if ((ptr = strchr(src, '/'))) {
78     *ptr++ = 0;
79     if ((skip = getval(ptr, min, (min ? max: max-1))) < 0) return 1;
80   }
81 
82   if (*src == '-' || *src == ',') return 1;
83   if (*src == '*') {
84     if (*(src+1)) return 1;
85   }
86   else {
87     for (;;) {
88       char *ctoken = strsep(&src, ","), *dtoken;
89 
90       if (!ctoken) break;
91       if (!*ctoken) return 1;
92 
93       // validate start position.
94       dtoken = strsep(&ctoken, "-");
95       if (isdigit(*dtoken)) {
96         if (getval(dtoken, min, (min ? max : max-1)) < 0) return 1;
97       } else if (getindex(dtoken, max) < 0) return 1;
98 
99       // validate end position.
100       if (!ctoken) {
101         if (skip) return 1; // case 10/20 or 1,2,4/3
102       }
103       else if (*ctoken) {// e.g. N-M
104         if (isdigit(*ctoken)) {
105           if (getval(ctoken, min, (min ? max : max-1)) < 0) return 1;
106         } else if (getindex(ctoken, max) < 0) return 1;
107       } else return 1; // error condition 'N-'
108     }
109   }
110   return 0;
111 }
112 
parse_crontab(char * fname)113 static int parse_crontab(char *fname)
114 {
115   char *line;
116   int lno, fd = xopen(fname, O_RDONLY);
117   long plen = 0;
118 
119   for (lno = 1; (line = get_rawline(fd, &plen, '\n')); lno++,free(line)) {
120     char *name, *val, *tokens[5] = {0,}, *ptr = line;
121     int count = 0;
122 
123     if (line[plen - 1] == '\n') line[--plen] = '\0';
124     else {
125       snprintf(toybuf, sizeof(toybuf), "'%d': premature EOF\n", lno);
126       goto OUT;
127     }
128 
129     ptr = omitspace(ptr);
130     if (!*ptr || *ptr == '#' || *ptr == '@') continue;
131     while (count<5) {
132       int len = strcspn(ptr, " \t");
133 
134       if (ptr[len]) ptr[len++] = '\0';
135       tokens[count++] = ptr;
136       ptr += len;
137       ptr = omitspace(ptr);
138       if (!*ptr) break;
139     }
140     switch (count) {
141       case 1: // form SHELL=/bin/sh
142         name = tokens[0];
143         if ((val = strchr(name, '='))) *val++ = 0;
144         if (!val || !*val) {
145           snprintf(toybuf, sizeof(toybuf), "'%d': %s\n", lno, line);
146           goto OUT;
147         }
148         break;
149       case 2: // form SHELL =/bin/sh or SHELL= /bin/sh
150         name = tokens[0];
151         if ((val = strchr(name, '='))) {
152           *val = 0;
153           val = tokens[1];
154         } else {
155           if (*(tokens[1]) != '=') {
156             snprintf(toybuf, sizeof(toybuf), "'%d': %s\n", lno, line);
157             goto OUT;
158           }
159           val = tokens[1] + 1;
160         }
161         if (!*val) {
162           snprintf(toybuf, sizeof(toybuf), "'%d': %s\n", lno, line);
163           goto OUT;
164         }
165         break;
166       case 3: // NAME = VAL
167         name = tokens[0];
168         val = tokens[2];
169         if (*(tokens[1]) != '=') {
170           snprintf(toybuf, sizeof(toybuf), "'%d': %s\n", lno, line);
171           goto OUT;
172         }
173         break;
174       default:
175         if (validate_component(0, 60, tokens[0])) {
176           snprintf(toybuf, sizeof(toybuf), "'%d': bad minute\n", lno);
177           goto OUT;
178         }
179         if (validate_component(0, 24, tokens[1])) {
180           snprintf(toybuf, sizeof(toybuf), "'%d': bad hour\n", lno);
181           goto OUT;
182         }
183         if (validate_component(1, 31, tokens[2])) {
184           snprintf(toybuf, sizeof(toybuf), "'%d': bad day-of-month\n", lno);
185           goto OUT;
186         }
187         if (validate_component(1, 12, tokens[3])) {
188           snprintf(toybuf, sizeof(toybuf), "'%d': bad month\n", lno);
189           goto OUT;
190         }
191         if (validate_component(0, 7, tokens[4])) {
192           snprintf(toybuf, sizeof(toybuf), "'%d': bad day-of-week\n", lno);
193           goto OUT;
194         }
195         if (!*ptr) { // don't have any cmd to execute.
196           snprintf(toybuf, sizeof(toybuf), "'%d': bad command\n", lno);
197           goto OUT;
198         }
199         break;
200     }
201   }
202   xclose(fd);
203   return 0;
204 OUT:
205   free(line);
206   printf("Error at line no %s", toybuf);
207   xclose(fd);
208   return 1;
209 }
210 
do_list(char * name)211 static void do_list(char *name)
212 {
213   int fdin;
214 
215   snprintf(toybuf, sizeof(toybuf), "%s%s", TT.cdir, name);
216   if ((fdin = open(toybuf, O_RDONLY)) == -1)
217     error_exit("No crontab for '%s'", name);
218   xsendfile(fdin, 1);
219   xclose(fdin);
220 }
221 
do_remove(char * name)222 static void do_remove(char *name)
223 {
224   snprintf(toybuf, sizeof(toybuf), "%s%s", TT.cdir, name);
225   if (unlink(toybuf))
226     error_exit("No crontab for '%s'", name);
227 }
228 
update_crontab(char * src,char * dest)229 static void update_crontab(char *src, char *dest)
230 {
231   int fdin, fdout;
232 
233   snprintf(toybuf, sizeof(toybuf), "%s%s", TT.cdir, dest);
234   fdout = xcreate(toybuf, O_WRONLY|O_CREAT|O_TRUNC, 0600);
235   fdin = xopen(src, O_RDONLY);
236   xsendfile(fdin, fdout);
237   xclose(fdin);
238 
239   fchown(fdout, getuid(), geteuid());
240   xclose(fdout);
241 }
242 
do_replace(char * name)243 static void do_replace(char *name)
244 {
245   char *fname = *toys.optargs ? *toys.optargs : "-";
246   char tname[] = "/tmp/crontab.XXXXXX";
247 
248   if ((*fname == '-') && !*(fname+1)) {
249     int tfd = mkstemp(tname);
250 
251     if (tfd < 0) perror_exit("mkstemp");
252     xsendfile(0, tfd);
253     xclose(tfd);
254     fname = tname;
255   }
256 
257   if (parse_crontab(fname))
258     error_exit("errors in crontab file '%s', can't install.", fname);
259   update_crontab(fname, name);
260   unlink(tname);
261 }
262 
do_edit(struct passwd * pwd)263 static void do_edit(struct passwd *pwd)
264 {
265   struct stat sb;
266   time_t mtime = 0;
267   int srcfd, destfd, status;
268   pid_t pid, cpid;
269   char tname[] = "/tmp/crontab.XXXXXX";
270 
271   if ((destfd = mkstemp(tname)) < 0)
272     perror_exit("Can't open tmp file");
273 
274   fchmod(destfd, 0666);
275   snprintf(toybuf, sizeof(toybuf), "%s%s", TT.cdir, pwd->pw_name);
276 
277   if (!stat(toybuf, &sb)) { // file exists and have some content.
278     if (sb.st_size) {
279       srcfd = xopen(toybuf, O_RDONLY);
280       xsendfile(srcfd, destfd);
281       xclose(srcfd);
282     }
283   } else printf("No crontab for '%s'- using an empty one\n", pwd->pw_name);
284   xclose(destfd);
285 
286   if (!stat(tname, &sb)) mtime = sb.st_mtime;
287 
288 RETRY:
289   if (!(pid = xfork())) {
290     char *prog = pwd->pw_shell;
291 
292     xsetuser(pwd);
293     if (pwd->pw_uid) {
294       if (setenv("USER", pwd->pw_name, 1)) _exit(1);
295       if (setenv("LOGNAME", pwd->pw_name, 1)) _exit(1);
296     }
297     if (setenv("HOME", pwd->pw_dir, 1)) _exit(1);
298     if (setenv("SHELL",((!prog || !*prog) ? "/bin/sh" : prog), 1)) _exit(1);
299 
300     if (!(prog = getenv("VISUAL"))) {
301       if (!(prog = getenv("EDITOR")))
302         prog = "vi";
303     }
304     execlp(prog, prog, tname, (char *) NULL);
305     perror_exit("can't execute '%s'", prog);
306   }
307 
308   // Parent Process.
309   do {
310     cpid = waitpid(pid, &status, 0);
311   } while ((cpid == -1) && (errno == EINTR));
312 
313   if (!stat(tname, &sb) && (mtime == sb.st_mtime)) {
314     printf("%s: no changes made to crontab\n", toys.which->name);
315     unlink(tname);
316     return;
317   }
318   printf("%s: installing new crontab\n", toys.which->name);
319   if (parse_crontab(tname)) {
320     fprintf(stderr, "errors in crontab file, can't install.\n"
321         "Do you want to retry the same edit? ");
322     if (!yesno(0)) {
323       error_msg("edits left in '%s'", tname);
324       return;
325     }
326     goto RETRY;
327   }
328   // parsing of crontab success; update the crontab.
329   update_crontab(tname, pwd->pw_name);
330   unlink(tname);
331 }
332 
crontab_main(void)333 void crontab_main(void)
334 {
335   struct passwd *pwd = NULL;
336   long FLAG_elr = toys.optflags & (FLAG_e|FLAG_l|FLAG_r);
337 
338   if (TT.cdir && (TT.cdir[strlen(TT.cdir)-1] != '/'))
339     TT.cdir = xmprintf("%s/", TT.cdir);
340   if (!TT.cdir) TT.cdir = xstrdup("/var/spool/cron/crontabs/");
341 
342   if (toys.optflags & FLAG_u) {
343     if (getuid()) error_exit("must be privileged to use -u");
344     pwd = xgetpwnam(TT.user);
345   } else pwd = xgetpwuid(getuid());
346 
347   if (!toys.optc) {
348     if (!FLAG_elr) {
349       if (toys.optflags & FLAG_u)
350         help_exit("file name must be specified for replace");
351       do_replace(pwd->pw_name);
352     }
353     else if (toys.optflags & FLAG_e) do_edit(pwd);
354     else if (toys.optflags & FLAG_l) do_list(pwd->pw_name);
355     else if (toys.optflags & FLAG_r) do_remove(pwd->pw_name);
356   } else {
357     if (FLAG_elr) help_exit("no arguments permitted after this option");
358     do_replace(pwd->pw_name);
359   }
360   if (!(toys.optflags & FLAG_c)) free(TT.cdir);
361 }
362