]> git.lizzy.rs Git - plan9front.git/blob - sys/src/cmd/auth/cron.c
9bootfat: rename open() to fileinit and make it static as its really a internal funct...
[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 "authcmdlib.h"
7
8 char CRONLOG[] = "cron";
9
10 enum {
11         Minute = 60,
12         Hour = 60 * Minute,
13         Day = 24 * Hour,
14 };
15
16 typedef struct Job      Job;
17 typedef struct Time     Time;
18 typedef struct User     User;
19
20 struct Time{                    /* bit masks for each valid time */
21         uvlong  min;
22         ulong   hour;
23         ulong   mday;
24         ulong   wday;
25         ulong   mon;
26 };
27
28 struct Job{
29         char    *host;          /* where ... */
30         Time    time;                   /* when ... */
31         char    *cmd;                   /* and what to execute */
32         Job     *next;
33 };
34
35 struct User{
36         Qid     lastqid;                        /* of last read /cron/user/cron */
37         char    *name;                  /* who ... */
38         Job     *jobs;                  /* wants to execute these jobs */
39 };
40
41 User    *users;
42 int     nuser;
43 int     maxuser;
44 char    *savec;
45 char    *savetok;
46 int     tok;
47 int     debug;
48 ulong   lexval;
49
50 void    rexec(User*, Job*);
51 void    readalljobs(void);
52 Job     *readjobs(char*, User*);
53 int     getname(char**);
54 uvlong  gettime(int, int);
55 int     gettok(int, int);
56 void    initcap(void);
57 void    pushtok(void);
58 void    usage(void);
59 void    freejobs(Job*);
60 User    *newuser(char*);
61 void    *emalloc(ulong);
62 void    *erealloc(void*, ulong);
63 int     myauth(int, char*);
64 void    createuser(void);
65 int     mkcmd(char*, char*, int);
66 void    printjobs(void);
67 int     qidcmp(Qid, Qid);
68 int     becomeuser(char*);
69
70 ulong
71 minute(ulong tm)
72 {
73         return tm - tm%Minute;          /* round down to the minute */
74 }
75
76 int
77 sleepuntil(ulong tm)
78 {
79         ulong now = time(0);
80         
81         if (now < tm)
82                 return sleep((tm - now)*1000);
83         else
84                 return 0;
85 }
86
87 #pragma varargck        argpos clog 1
88 #pragma varargck        argpos fatal 1
89
90 static void
91 clog(char *fmt, ...)
92 {
93         char msg[256];
94         va_list arg;
95
96         va_start(arg, fmt);
97         vseprint(msg, msg + sizeof msg, fmt, arg);
98         va_end(arg);
99         syslog(0, CRONLOG, msg);
100 }
101
102 static void
103 fatal(char *fmt, ...)
104 {
105         char msg[256];
106         va_list arg;
107
108         va_start(arg, fmt);
109         vseprint(msg, msg + sizeof msg, fmt, arg);
110         va_end(arg);
111         clog("%s", msg);
112         error("%s", msg);
113 }
114
115 static int
116 openlock(char *file)
117 {
118         return create(file, ORDWR, 0600);
119 }
120
121 static int
122 mklock(char *file)
123 {
124         int fd, try;
125         Dir *dir;
126
127         fd = openlock(file);
128         if (fd >= 0) {
129                 /* make it a lock file if it wasn't */
130                 dir = dirfstat(fd);
131                 if (dir == nil)
132                         error("%s vanished: %r", file);
133                 dir->mode |= DMEXCL;
134                 dir->qid.type |= QTEXCL;
135                 dirfwstat(fd, dir);
136                 free(dir);
137
138                 /* reopen in case it wasn't a lock file at last open */
139                 close(fd);
140         }
141         for (try = 0; try < 65 && (fd = openlock(file)) < 0; try++)
142                 sleep(10*1000);
143         return fd;
144 }
145
146 void
147 main(int argc, char *argv[])
148 {
149         Job *j;
150         Tm tm;
151         Time t;
152         ulong now, last;                /* in seconds */
153         int i, lock;
154
155         debug = 0;
156         ARGBEGIN{
157         case 'c':
158                 createuser();
159                 exits(0);
160         case 'd':
161                 debug = 1;
162                 break;
163         default:
164                 usage();
165         }ARGEND
166
167         if(debug){
168                 readalljobs();
169                 printjobs();
170                 exits(0);
171         }
172
173         initcap();              /* do this early, before cpurc removes it */
174
175         switch(fork()){
176         case -1:
177                 fatal("can't fork");
178         case 0:
179                 break;
180         default:
181                 exits(0);
182         }
183
184         /*
185          * it can take a few minutes before the file server notices that
186          * we've rebooted and gives up the lock.
187          */
188         lock = mklock("/cron/lock");
189         if (lock < 0)
190                 fatal("cron already running: %r");
191
192         argv0 = "cron";
193         srand(getpid()*time(0));
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         sprint(file, "/cron/%s", user);
252         fd = create(file, OREAD, 0755|DMDIR);
253         if(fd < 0)
254                 sysfatal("couldn't create %s: %r", file);
255         nulldir(&d);
256         d.gid = user;
257         dirfwstat(fd, &d);
258         close(fd);
259         sprint(file, "/cron/%s/cron", user);
260         fd = create(file, OREAD, 0644);
261         if(fd < 0)
262                 sysfatal("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\n");
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                         sprint(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 int
504 call(char *host)
505 {
506         char *na, *p;
507
508         na = netmkaddr(host, 0, "rexexec");
509         p = utfrune(na, L'!');
510         if(!p)
511                 return -1;
512         p = utfrune(p+1, L'!');
513         if(!p)
514                 return -1;
515         if(strcmp(p, "!rexexec") != 0)
516                 return -2;
517         return dial(na, 0, 0, 0);
518 }
519
520 /*
521  * convert command to run properly on the remote machine
522  * need to escape the quotes so they don't get stripped
523  */
524 int
525 mkcmd(char *cmd, char *buf, int len)
526 {
527         char *p;
528         int n, m;
529
530         n = sizeof "exec rc -c '" -1;
531         if(n >= len)
532                 return 0;
533         strcpy(buf, "exec rc -c '");
534         while(p = utfrune(cmd, L'\'')){
535                 p++;
536                 m = p - cmd;
537                 if(n + m + 1 >= len)
538                         return 0;
539                 strncpy(&buf[n], cmd, m);
540                 n += m;
541                 buf[n++] = '\'';
542                 cmd = p;
543         }
544         m = strlen(cmd);
545         if(n + m + sizeof "'</dev/null>/dev/null>[2=1]" >= len)
546                 return 0;
547         strcpy(&buf[n], cmd);
548         strcpy(&buf[n+m], "'</dev/null>/dev/null>[2=1]");
549         return 1;
550 }
551
552 void
553 rexec(User *user, Job *j)
554 {
555         char buf[8*1024];
556         int n, fd;
557         AuthInfo *ai;
558
559         switch(rfork(RFPROC|RFNOWAIT|RFNAMEG|RFENVG|RFFDG)){
560         case 0:
561                 break;
562         case -1:
563                 clog("can't fork a job for %s: %r\n", user->name);
564         default:
565                 return;
566         }
567
568         if(!mkcmd(j->cmd, buf, sizeof buf)){
569                 clog("internal error: cmd buffer overflow");
570                 _exits(0);
571         }
572
573         /*
574          * local call, auth, cmd with no i/o
575          */
576         if(strcmp(j->host, "local") == 0){
577                 if(becomeuser(user->name) < 0){
578                         clog("%s: can't change uid for %s on %s: %r",
579                                 user->name, j->cmd, j->host);
580                         _exits(0);
581                 }
582                 putenv("service", "rx");
583                 clog("%s: ran '%s' on %s", user->name, j->cmd, j->host);
584                 execl("/bin/rc", "rc", "-lc", buf, nil);
585                 clog("%s: exec failed for %s on %s: %r",
586                         user->name, j->cmd, j->host);
587                 _exits(0);
588         }
589
590         /*
591          * remote call, auth, cmd with no i/o
592          * give it 2 min to complete
593          */
594         alarm(2*Minute*1000);
595         fd = call(j->host);
596         if(fd < 0){
597                 if(fd == -2)
598                         clog("%s: dangerous host %s", user->name, j->host);
599                 clog("%s: can't call %s: %r", user->name, j->host);
600                 _exits(0);
601         }
602         clog("%s: called %s on %s", user->name, j->cmd, j->host);
603         if(becomeuser(user->name) < 0){
604                 clog("%s: can't change uid for %s on %s: %r",
605                         user->name, j->cmd, j->host);
606                 _exits(0);
607         }
608         ai = auth_proxy(fd, nil, "proto=p9any role=client");
609         if(ai == nil){
610                 clog("%s: can't authenticate for %s on %s: %r",
611                         user->name, j->cmd, j->host);
612                 _exits(0);
613         }
614         clog("%s: authenticated %s on %s", user->name, j->cmd, j->host);
615         write(fd, buf, strlen(buf)+1);
616         write(fd, buf, 0);
617         while((n = read(fd, buf, sizeof(buf)-1)) > 0){
618                 buf[n] = 0;
619                 clog("%s: %s\n", j->cmd, buf);
620         }
621         _exits(0);
622 }
623
624 void *
625 emalloc(ulong n)
626 {
627         void *p;
628
629         if(p = mallocz(n, 1))
630                 return p;
631         fatal("out of memory");
632         return 0;
633 }
634
635 void *
636 erealloc(void *p, ulong n)
637 {
638         if(p = realloc(p, n))
639                 return p;
640         fatal("out of memory");
641         return 0;
642 }
643
644 void
645 usage(void)
646 {
647         fprint(2, "usage: cron [-c]\n");
648         exits("usage");
649 }
650
651 int
652 qidcmp(Qid a, Qid b)
653 {
654         /* might be useful to know if a > b, but not for cron */
655         return(a.path != b.path || a.vers != b.vers);
656 }
657
658 void
659 memrandom(void *p, int n)
660 {
661         uchar *cp;
662
663         for(cp = (uchar*)p; n > 0; n--)
664                 *cp++ = fastrand();
665 }
666
667 /*
668  *  keep caphash fd open since opens of it could be disabled
669  */
670 static int caphashfd;
671
672 void
673 initcap(void)
674 {
675         caphashfd = open("#¤/caphash", OCEXEC|OWRITE);
676         if(caphashfd < 0)
677                 fprint(2, "%s: opening #¤/caphash: %r\n", argv0);
678 }
679
680 /*
681  *  create a change uid capability 
682  */
683 char*
684 mkcap(char *from, char *to)
685 {
686         uchar rand[20];
687         char *cap;
688         char *key;
689         int nfrom, nto;
690         uchar hash[SHA1dlen];
691
692         if(caphashfd < 0)
693                 return nil;
694
695         /* create the capability */
696         nto = strlen(to);
697         nfrom = strlen(from);
698         cap = emalloc(nfrom+1+nto+1+sizeof(rand)*3+1);
699         sprint(cap, "%s@%s", from, to);
700         memrandom(rand, sizeof(rand));
701         key = cap+nfrom+1+nto+1;
702         enc64(key, sizeof(rand)*3, rand, sizeof(rand));
703
704         /* hash the capability */
705         hmac_sha1((uchar*)cap, strlen(cap), (uchar*)key, strlen(key), hash, nil);
706
707         /* give the kernel the hash */
708         key[-1] = '@';
709         if(write(caphashfd, hash, SHA1dlen) < 0){
710                 free(cap);
711                 return nil;
712         }
713
714         return cap;
715 }
716
717 int
718 usecap(char *cap)
719 {
720         int fd, rv;
721
722         fd = open("#¤/capuse", OWRITE);
723         if(fd < 0)
724                 return -1;
725         rv = write(fd, cap, strlen(cap));
726         close(fd);
727         return rv;
728 }
729
730 int
731 becomeuser(char *new)
732 {
733         char *cap;
734         int rv;
735
736         cap = mkcap(getuser(), new);
737         if(cap == nil)
738                 return -1;
739         rv = usecap(cap);
740         free(cap);
741
742         newns(new, nil);
743         return rv;
744 }