xref: /plan9/sys/src/cmd/auth/cron.c (revision 4377bcc77cf349d5aeb8afff427e607ef3654aab)
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
minute(ulong tm)71 minute(ulong tm)
72 {
73 	return tm - tm%Minute;		/* round down to the minute */
74 }
75 
76 int
sleepuntil(ulong tm)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
clog(char * fmt,...)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
fatal(char * fmt,...)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
openlock(char * file)116 openlock(char *file)
117 {
118 	return create(file, ORDWR, 0600);
119 }
120 
121 static int
mklock(char * file)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
main(int argc,char * argv[])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
createuser(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
readalljobs(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 *
readjobs(char * file,User * user)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
printjobs(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 *
newuser(char * name)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
freejobs(Job * j)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
getname(char ** namep)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
gettime(int min,int max)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
pushtok(void)473 pushtok(void)
474 {
475 	savec = savetok;
476 }
477 
478 int
gettok(int min,int max)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
call(char * host)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
mkcmd(char * cmd,char * buf,int len)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
rexec(User * user,Job * j)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 *
emalloc(ulong n)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 *
erealloc(void * p,ulong n)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
usage(void)645 usage(void)
646 {
647 	fprint(2, "usage: cron [-c]\n");
648 	exits("usage");
649 }
650 
651 int
qidcmp(Qid a,Qid b)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
memrandom(void * p,int n)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
initcap(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*
mkcap(char * from,char * to)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
usecap(char * cap)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
becomeuser(char * new)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