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: %r"); 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 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 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, ncap; 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 ncap = nfrom + 1 + nto + 1 + sizeof(rand)*3 + 1; 699 cap = emalloc(ncap); 700 snprint(cap, ncap, "%s@%s", from, to); 701 memrandom(rand, sizeof(rand)); 702 key = cap+nfrom+1+nto+1; 703 enc64(key, sizeof(rand)*3, rand, sizeof(rand)); 704 705 /* hash the capability */ 706 hmac_sha1((uchar*)cap, strlen(cap), (uchar*)key, strlen(key), hash, nil); 707 708 /* give the kernel the hash */ 709 key[-1] = '@'; 710 if(write(caphashfd, hash, SHA1dlen) < 0){ 711 free(cap); 712 return nil; 713 } 714 715 return cap; 716 } 717 718 int 719 usecap(char *cap) 720 { 721 int fd, rv; 722 723 fd = open("#¤/capuse", OWRITE); 724 if(fd < 0) 725 return -1; 726 rv = write(fd, cap, strlen(cap)); 727 close(fd); 728 return rv; 729 } 730 731 int 732 becomeuser(char *new) 733 { 734 char *cap; 735 int rv; 736 737 cap = mkcap(getuser(), new); 738 if(cap == nil) 739 return -1; 740 rv = usecap(cap); 741 free(cap); 742 743 newns(new, nil); 744 return rv; 745 } 746