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