xref: /plan9/sys/src/cmd/auth/cron.c (revision 8deabd962e84f51c67a12f970084955d97d8a8f2)
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