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