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