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