]> git.lizzy.rs Git - plan9front.git/blob - sys/src/cmd/auth/cron.c
merge
[plan9front.git] / sys / src / cmd / auth / cron.c
1 #include <u.h>
2 #include <libc.h>
3 #include <bio.h>
4 #include <libsec.h>
5 #include <auth.h>
6 #include <authsrv.h>
7 #include "authcmdlib.h"
8
9 char CRONLOG[] = "cron";
10
11 enum {
12         Minute = 60,
13         Hour = 60 * Minute,
14         Day = 24 * Hour,
15 };
16
17 typedef struct Job      Job;
18 typedef struct Time     Time;
19 typedef struct User     User;
20
21 struct Time{                    /* bit masks for each valid time */
22         uvlong  min;
23         ulong   hour;
24         ulong   mday;
25         ulong   wday;
26         ulong   mon;
27 };
28
29 struct Job{
30         char    *host;          /* where ... */
31         Time    time;                   /* when ... */
32         char    *cmd;                   /* and what to execute */
33         Job     *next;
34 };
35
36 struct User{
37         Qid     lastqid;                        /* of last read /cron/user/cron */
38         char    *name;                  /* who ... */
39         Job     *jobs;                  /* wants to execute these jobs */
40 };
41
42 User    *users;
43 int     nuser;
44 int     maxuser;
45 char    *savec;
46 char    *savetok;
47 int     tok;
48 int     debug;
49 ulong   lexval;
50
51 void    rexec(User*, Job*);
52 void    readalljobs(void);
53 Job     *readjobs(char*, User*);
54 int     getname(char**);
55 uvlong  gettime(int, int);
56 int     gettok(int, int);
57 void    initcap(void);
58 void    pushtok(void);
59 void    usage(void);
60 void    freejobs(Job*);
61 User    *newuser(char*);
62 void    *emalloc(ulong);
63 void    *erealloc(void*, ulong);
64 int     myauth(int, char*);
65 void    createuser(void);
66 int     mkcmd(char*, char*, int);
67 void    printjobs(void);
68 int     qidcmp(Qid, Qid);
69 int     becomeuser(char*);
70
71 ulong
72 minute(ulong tm)
73 {
74         return tm - tm%Minute;          /* round down to the minute */
75 }
76
77 int
78 sleepuntil(ulong tm)
79 {
80         ulong now = time(0);
81         
82         if (now < tm)
83                 return sleep((tm - now)*1000);
84         else
85                 return 0;
86 }
87
88 #pragma varargck        argpos clog 1
89 #pragma varargck        argpos fatal 1
90
91 static void
92 clog(char *fmt, ...)
93 {
94         char msg[256];
95         va_list arg;
96
97         va_start(arg, fmt);
98         vseprint(msg, msg + sizeof msg, fmt, arg);
99         va_end(arg);
100         syslog(0, CRONLOG, msg);
101 }
102
103 static void
104 fatal(char *fmt, ...)
105 {
106         char msg[256];
107         va_list arg;
108
109         va_start(arg, fmt);
110         vseprint(msg, msg + sizeof msg, fmt, arg);
111         va_end(arg);
112         clog("%s", msg);
113         error("%s", msg);
114 }
115
116 static int
117 openlock(char *file)
118 {
119         return create(file, ORDWR, 0600);
120 }
121
122 static int
123 mklock(char *file)
124 {
125         int fd, try;
126         Dir *dir;
127
128         fd = openlock(file);
129         if (fd >= 0) {
130                 /* make it a lock file if it wasn't */
131                 dir = dirfstat(fd);
132                 if (dir == nil)
133                         error("%s vanished: %r", file);
134                 dir->mode |= DMEXCL;
135                 dir->qid.type |= QTEXCL;
136                 dirfwstat(fd, dir);
137                 free(dir);
138
139                 /* reopen in case it wasn't a lock file at last open */
140                 close(fd);
141         }
142         for (try = 0; try < 65 && (fd = openlock(file)) < 0; try++)
143                 sleep(10*1000);
144         return fd;
145 }
146
147 void
148 main(int argc, char *argv[])
149 {
150         Job *j;
151         Tm tm;
152         Time t;
153         ulong now, last;                /* in seconds */
154         int i, lock;
155
156         debug = 0;
157         ARGBEGIN{
158         case 'c':
159                 createuser();
160                 exits(0);
161         case 'd':
162                 debug = 1;
163                 break;
164         default:
165                 usage();
166         }ARGEND
167
168         if(debug){
169                 readalljobs();
170                 printjobs();
171                 exits(0);
172         }
173
174         initcap();              /* do this early, before cpurc removes it */
175
176         switch(fork()){
177         case -1:
178                 fatal("can't fork: %r");
179         case 0:
180                 break;
181         default:
182                 exits(0);
183         }
184
185         /*
186          * it can take a few minutes before the file server notices that
187          * we've rebooted and gives up the lock.
188          */
189         lock = mklock("/cron/lock");
190         if (lock < 0)
191                 fatal("cron already running: %r");
192
193         argv0 = "cron";
194         last = time(0);
195         for(;;){
196                 readalljobs();
197                 /*
198                  * the system's notion of time may have jumped forward or
199                  * backward an arbitrary amount since the last call to time().
200                  */
201                 now = time(0);
202                 /*
203                  * if time has jumped backward, just note it and adapt.
204                  * if time has jumped forward more than a day,
205                  * just execute one day's jobs.
206                  */
207                 if (now < last) {
208                         clog("time went backward");
209                         last = now;
210                 } else if (now - last > Day) {
211                         clog("time advanced more than a day");
212                         last = now - Day;
213                 }
214                 now = minute(now);
215                 for(last = minute(last); last <= now; last += Minute){
216                         tm = *localtime(last);
217                         t.min = 1ULL << tm.min;
218                         t.hour = 1 << tm.hour;
219                         t.wday = 1 << tm.wday;
220                         t.mday = 1 << tm.mday;
221                         t.mon =  1 << (tm.mon + 1);
222                         for(i = 0; i < nuser; i++)
223                                 for(j = users[i].jobs; j; j = j->next)
224                                         if(j->time.min & t.min
225                                         && j->time.hour & t.hour
226                                         && j->time.wday & t.wday
227                                         && j->time.mday & t.mday
228                                         && j->time.mon & t.mon)
229                                                 rexec(&users[i], j);
230                 }
231                 seek(lock, 0, 0);
232                 write(lock, "x", 1);    /* keep the lock alive */
233                 /*
234                  * if we're not at next minute yet, sleep until a second past
235                  * (to allow for sleep intervals being approximate),
236                  * which synchronises with minute roll-over as a side-effect.
237                  */
238                 sleepuntil(now + Minute + 1);
239         }
240         /* not reached */
241 }
242
243 void
244 createuser(void)
245 {
246         Dir d;
247         char file[128], *user;
248         int fd;
249
250         user = getuser();
251         snprint(file, sizeof file, "/cron/%s", user);
252         fd = create(file, OREAD, 0755|DMDIR);
253         if(fd < 0)
254                 fatal("couldn't create %s: %r", file);
255         nulldir(&d);
256         d.gid = user;
257         dirfwstat(fd, &d);
258         close(fd);
259         snprint(file, sizeof file, "/cron/%s/cron", user);
260         fd = create(file, OREAD, 0644);
261         if(fd < 0)
262                 fatal("couldn't create %s: %r", file);
263         nulldir(&d);
264         d.gid = user;
265         dirfwstat(fd, &d);
266         close(fd);
267 }
268
269 void
270 readalljobs(void)
271 {
272         User *u;
273         Dir *d, *du;
274         char file[128];
275         int i, n, fd;
276
277         fd = open("/cron", OREAD);
278         if(fd < 0)
279                 fatal("can't open /cron: %r");
280         while((n = dirread(fd, &d)) > 0){
281                 for(i = 0; i < n; i++){
282                         if(strcmp(d[i].name, "log") == 0 ||
283                             !(d[i].qid.type & QTDIR))
284                                 continue;
285                         if(strcmp(d[i].name, d[i].uid) != 0){
286                                 syslog(1, CRONLOG, "cron for %s owned by %s",
287                                         d[i].name, d[i].uid);
288                                 continue;
289                         }
290                         u = newuser(d[i].name);
291                         snprint(file, sizeof file, "/cron/%s/cron", d[i].name);
292                         du = dirstat(file);
293                         if(du == nil || qidcmp(u->lastqid, du->qid) != 0){
294                                 freejobs(u->jobs);
295                                 u->jobs = readjobs(file, u);
296                         }
297                         free(du);
298                 }
299                 free(d);
300         }
301         close(fd);
302 }
303
304 /*
305  * parse user's cron file
306  * other lines: minute hour monthday month weekday host command
307  */
308 Job *
309 readjobs(char *file, User *user)
310 {
311         Biobuf *b;
312         Job *j, *jobs;
313         Dir *d;
314         int line;
315
316         d = dirstat(file);
317         if(!d)
318                 return nil;
319         b = Bopen(file, OREAD);
320         if(!b){
321                 free(d);
322                 return nil;
323         }
324         jobs = nil;
325         user->lastqid = d->qid;
326         free(d);
327         for(line = 1; savec = Brdline(b, '\n'); line++){
328                 savec[Blinelen(b) - 1] = '\0';
329                 while(*savec == ' ' || *savec == '\t')
330                         savec++;
331                 if(*savec == '#' || *savec == '\0')
332                         continue;
333                 if(strlen(savec) > 1024){
334                         clog("%s: line %d: line too long", user->name, line);
335                         continue;
336                 }
337                 j = emalloc(sizeof *j);
338                 j->time.min = gettime(0, 59);
339                 if(j->time.min && (j->time.hour = gettime(0, 23))
340                 && (j->time.mday = gettime(1, 31))
341                 && (j->time.mon = gettime(1, 12))
342                 && (j->time.wday = gettime(0, 6))
343                 && getname(&j->host)){
344                         j->cmd = emalloc(strlen(savec) + 1);
345                         strcpy(j->cmd, savec);
346                         j->next = jobs;
347                         jobs = j;
348                 }else{
349                         clog("%s: line %d: syntax error", user->name, line);
350                         free(j);
351                 }
352         }
353         Bterm(b);
354         return jobs;
355 }
356
357 void
358 printjobs(void)
359 {
360         char buf[8*1024];
361         Job *j;
362         int i;
363
364         for(i = 0; i < nuser; i++){
365                 print("user %s\n", users[i].name);
366                 for(j = users[i].jobs; j; j = j->next)
367                         if(!mkcmd(j->cmd, buf, sizeof buf))
368                                 print("\tbad job %s on host %s\n",
369                                         j->cmd, j->host);
370                         else
371                                 print("\tjob %s on host %s\n", buf, j->host);
372         }
373 }
374
375 User *
376 newuser(char *name)
377 {
378         int i;
379
380         for(i = 0; i < nuser; i++)
381                 if(strcmp(users[i].name, name) == 0)
382                         return &users[i];
383         if(nuser == maxuser){
384                 maxuser += 32;
385                 users = erealloc(users, maxuser * sizeof *users);
386         }
387         memset(&users[nuser], 0, sizeof(users[nuser]));
388         users[nuser].name = strdup(name);
389         users[nuser].jobs = 0;
390         users[nuser].lastqid.type = QTFILE;
391         users[nuser].lastqid.path = ~0LL;
392         users[nuser].lastqid.vers = ~0L;
393         return &users[nuser++];
394 }
395
396 void
397 freejobs(Job *j)
398 {
399         Job *next;
400
401         for(; j; j = next){
402                 next = j->next;
403                 free(j->cmd);
404                 free(j->host);
405                 free(j);
406         }
407 }
408
409 int
410 getname(char **namep)
411 {
412         int c;
413         char buf[64], *p;
414
415         if(!savec)
416                 return 0;
417         while(*savec == ' ' || *savec == '\t')
418                 savec++;
419         for(p = buf; (c = *savec) && c != ' ' && c != '\t'; p++){
420                 if(p >= buf+sizeof buf -1)
421                         return 0;
422                 *p = *savec++;
423         }
424         *p = '\0';
425         *namep = strdup(buf);
426         if(*namep == 0){
427                 clog("internal error: strdup failure");
428                 _exits(0);
429         }
430         while(*savec == ' ' || *savec == '\t')
431                 savec++;
432         return p > buf;
433 }
434
435 /*
436  * return the next time range (as a bit vector) in the file:
437  * times: '*'
438  *      | range
439  * range: number
440  *      | number '-' number
441  *      | range ',' range
442  * a return of zero means a syntax error was discovered
443  */
444 uvlong
445 gettime(int min, int max)
446 {
447         uvlong n, m, e;
448
449         if(gettok(min, max) == '*')
450                 return ~0ULL;
451         n = 0;
452         while(tok == '1'){
453                 m = 1ULL << lexval;
454                 n |= m;
455                 if(gettok(0, 0) == '-'){
456                         if(gettok(lexval, max) != '1')
457                                 return 0;
458                         e = 1ULL << lexval;
459                         for( ; m <= e; m <<= 1)
460                                 n |= m;
461                         gettok(min, max);
462                 }
463                 if(tok != ',')
464                         break;
465                 if(gettok(min, max) != '1')
466                         return 0;
467         }
468         pushtok();
469         return n;
470 }
471
472 void
473 pushtok(void)
474 {
475         savec = savetok;
476 }
477
478 int
479 gettok(int min, int max)
480 {
481         char c;
482
483         savetok = savec;
484         if(!savec)
485                 return tok = 0;
486         while((c = *savec) == ' ' || c == '\t')
487                 savec++;
488         switch(c){
489         case '0': case '1': case '2': case '3': case '4':
490         case '5': case '6': case '7': case '8': case '9':
491                 lexval = strtoul(savec, &savec, 10);
492                 if(lexval < min || lexval > max)
493                         return tok = 0;
494                 return tok = '1';
495         case '*': case '-': case ',':
496                 savec++;
497                 return tok = c;
498         default:
499                 return tok = 0;
500         }
501 }
502
503 /*
504  * convert command to run properly on the remote machine
505  * need to escape the quotes so they don't get stripped
506  */
507 int
508 mkcmd(char *cmd, char *buf, int len)
509 {
510         char *p;
511         int n, m;
512
513         n = sizeof "exec rc -c '" -1;
514         if(n >= len)
515                 return 0;
516         strcpy(buf, "exec rc -c '");
517         while(p = utfrune(cmd, L'\'')){
518                 p++;
519                 m = p - cmd;
520                 if(n + m + 1 >= len)
521                         return 0;
522                 strncpy(&buf[n], cmd, m);
523                 n += m;
524                 buf[n++] = '\'';
525                 cmd = p;
526         }
527         m = strlen(cmd);
528         if(n + m + sizeof "'</dev/null>/dev/null>[2=1]" >= len)
529                 return 0;
530         strcpy(&buf[n], cmd);
531         strcpy(&buf[n+m], "'</dev/null>/dev/null>[2=1]");
532         return 1;
533 }
534
535 void
536 rexec(User *user, Job *j)
537 {
538         char buf[8*1024];
539
540         switch(rfork(RFPROC|RFNOWAIT|RFNAMEG|RFENVG|RFFDG)){
541         case 0:
542                 break;
543         case -1:
544                 clog("can't fork a job for %s: %r\n", user->name);
545         default:
546                 return;
547         }
548
549         if(!mkcmd(j->cmd, buf, sizeof buf)){
550                 clog("internal error: cmd buffer overflow");
551                 _exits(0);
552         }
553
554         if(becomeuser(user->name) < 0){
555                 clog("%s: can't change uid for %s on %s: %r",
556                         user->name, j->cmd, j->host);
557                 _exits(0);
558         }
559
560         clog("%s: ran '%s' on %s", user->name, j->cmd, j->host);
561
562         close(0);
563         close(1);
564         close(2);
565         open("/dev/null", OREAD);
566         open("/dev/null", OWRITE);
567         open("/dev/null", OWRITE);
568
569         if(strcmp(j->host, "local") == 0){
570                 putenv("service", "rx");
571                 execl("/bin/rc", "rc", "-lc", buf, nil);
572         } else {
573                 execl("/bin/rx", "rx", j->host, buf, nil);
574         }
575
576         clog("%s: exec failed for %s on %s: %r", user->name, j->cmd, j->host);
577         _exits(0);
578 }
579
580 void *
581 emalloc(ulong n)
582 {
583         void *p;
584
585         if(p = mallocz(n, 1))
586                 return p;
587         fatal("out of memory");
588         return 0;
589 }
590
591 void *
592 erealloc(void *p, ulong n)
593 {
594         if(p = realloc(p, n))
595                 return p;
596         fatal("out of memory");
597         return 0;
598 }
599
600 void
601 usage(void)
602 {
603         fprint(2, "usage: cron [-c]\n");
604         exits("usage");
605 }
606
607 int
608 qidcmp(Qid a, Qid b)
609 {
610         /* might be useful to know if a > b, but not for cron */
611         return(a.path != b.path || a.vers != b.vers);
612 }
613
614 /*
615  *  keep caphash fd open since opens of it could be disabled
616  */
617 static int caphashfd;
618
619 void
620 initcap(void)
621 {
622         caphashfd = open("#¤/caphash", OCEXEC|OWRITE);
623         if(caphashfd < 0)
624                 fprint(2, "%s: opening #¤/caphash: %r\n", argv0);
625 }
626
627 /*
628  *  create a change uid capability 
629  */
630 char*
631 mkcap(char *from, char *to)
632 {
633         uchar rand[20];
634         char *cap;
635         char *key;
636         int nfrom, nto, ncap;
637         uchar hash[SHA1dlen];
638
639         if(caphashfd < 0)
640                 return nil;
641
642         /* create the capability */
643         nto = strlen(to);
644         nfrom = strlen(from);
645         ncap = nfrom + 1 + nto + 1 + sizeof(rand)*3 + 1;
646         cap = emalloc(ncap);
647         snprint(cap, ncap, "%s@%s", from, to);
648         genrandom(rand, sizeof(rand));
649         key = cap+nfrom+1+nto+1;
650         enc64(key, sizeof(rand)*3, rand, sizeof(rand));
651
652         /* hash the capability */
653         hmac_sha1((uchar*)cap, strlen(cap), (uchar*)key, strlen(key), hash, nil);
654
655         /* give the kernel the hash */
656         key[-1] = '@';
657         if(write(caphashfd, hash, SHA1dlen) < 0){
658                 free(cap);
659                 return nil;
660         }
661
662         return cap;
663 }
664
665 int
666 usecap(char *cap)
667 {
668         int fd, rv;
669
670         fd = open("#¤/capuse", OWRITE);
671         if(fd < 0)
672                 return -1;
673         rv = write(fd, cap, strlen(cap));
674         close(fd);
675         return rv;
676 }
677
678 int
679 becomeuser(char *new)
680 {
681         char *cap;
682         int rv;
683
684         cap = mkcap(getuser(), new);
685         if(cap == nil)
686                 return -1;
687         rv = usecap(cap);
688         free(cap);
689
690         newns(new, nil);
691         return rv;
692 }