plan9fox/sys/src/cmd/auth/cron.c
cinap_lenrek 02cfcfeab4 libauthsrv: generalize ticket service, not hardcoding ticket format and DES encryption
this is in preparation for replacing DES ticket encryption with
something better. but first need to make the code stop making
assumptions.

the wire encoding of the Ticket might be variable length
with TICKETLEN just giving an upper bound. the details will be
handled by libauthsrv _asgetticket() and _asgetresp() funciotns.

the Authenticator and Passwordreq structures are encrypted
with the random ticket key. The encryption schmeme will depend
on the Ticket format used, so we pass the Ticket* structure
instead of the DES key.

introduce Authkey structure that will hold all the required
cryptographic keys instead of passing DES key.
2015-08-19 21:06:17 +02:00

746 lines
13 KiB
C

#include <u.h>
#include <libc.h>
#include <bio.h>
#include <libsec.h>
#include <auth.h>
#include <authsrv.h>
#include "authcmdlib.h"
char CRONLOG[] = "cron";
enum {
Minute = 60,
Hour = 60 * Minute,
Day = 24 * Hour,
};
typedef struct Job Job;
typedef struct Time Time;
typedef struct User User;
struct Time{ /* bit masks for each valid time */
uvlong min;
ulong hour;
ulong mday;
ulong wday;
ulong mon;
};
struct Job{
char *host; /* where ... */
Time time; /* when ... */
char *cmd; /* and what to execute */
Job *next;
};
struct User{
Qid lastqid; /* of last read /cron/user/cron */
char *name; /* who ... */
Job *jobs; /* wants to execute these jobs */
};
User *users;
int nuser;
int maxuser;
char *savec;
char *savetok;
int tok;
int debug;
ulong lexval;
void rexec(User*, Job*);
void readalljobs(void);
Job *readjobs(char*, User*);
int getname(char**);
uvlong gettime(int, int);
int gettok(int, int);
void initcap(void);
void pushtok(void);
void usage(void);
void freejobs(Job*);
User *newuser(char*);
void *emalloc(ulong);
void *erealloc(void*, ulong);
int myauth(int, char*);
void createuser(void);
int mkcmd(char*, char*, int);
void printjobs(void);
int qidcmp(Qid, Qid);
int becomeuser(char*);
ulong
minute(ulong tm)
{
return tm - tm%Minute; /* round down to the minute */
}
int
sleepuntil(ulong tm)
{
ulong now = time(0);
if (now < tm)
return sleep((tm - now)*1000);
else
return 0;
}
#pragma varargck argpos clog 1
#pragma varargck argpos fatal 1
static void
clog(char *fmt, ...)
{
char msg[256];
va_list arg;
va_start(arg, fmt);
vseprint(msg, msg + sizeof msg, fmt, arg);
va_end(arg);
syslog(0, CRONLOG, msg);
}
static void
fatal(char *fmt, ...)
{
char msg[256];
va_list arg;
va_start(arg, fmt);
vseprint(msg, msg + sizeof msg, fmt, arg);
va_end(arg);
clog("%s", msg);
error("%s", msg);
}
static int
openlock(char *file)
{
return create(file, ORDWR, 0600);
}
static int
mklock(char *file)
{
int fd, try;
Dir *dir;
fd = openlock(file);
if (fd >= 0) {
/* make it a lock file if it wasn't */
dir = dirfstat(fd);
if (dir == nil)
error("%s vanished: %r", file);
dir->mode |= DMEXCL;
dir->qid.type |= QTEXCL;
dirfwstat(fd, dir);
free(dir);
/* reopen in case it wasn't a lock file at last open */
close(fd);
}
for (try = 0; try < 65 && (fd = openlock(file)) < 0; try++)
sleep(10*1000);
return fd;
}
void
main(int argc, char *argv[])
{
Job *j;
Tm tm;
Time t;
ulong now, last; /* in seconds */
int i, lock;
debug = 0;
ARGBEGIN{
case 'c':
createuser();
exits(0);
case 'd':
debug = 1;
break;
default:
usage();
}ARGEND
if(debug){
readalljobs();
printjobs();
exits(0);
}
initcap(); /* do this early, before cpurc removes it */
switch(fork()){
case -1:
fatal("can't fork: %r");
case 0:
break;
default:
exits(0);
}
/*
* it can take a few minutes before the file server notices that
* we've rebooted and gives up the lock.
*/
lock = mklock("/cron/lock");
if (lock < 0)
fatal("cron already running: %r");
argv0 = "cron";
srand(getpid()*time(0));
last = time(0);
for(;;){
readalljobs();
/*
* the system's notion of time may have jumped forward or
* backward an arbitrary amount since the last call to time().
*/
now = time(0);
/*
* if time has jumped backward, just note it and adapt.
* if time has jumped forward more than a day,
* just execute one day's jobs.
*/
if (now < last) {
clog("time went backward");
last = now;
} else if (now - last > Day) {
clog("time advanced more than a day");
last = now - Day;
}
now = minute(now);
for(last = minute(last); last <= now; last += Minute){
tm = *localtime(last);
t.min = 1ULL << tm.min;
t.hour = 1 << tm.hour;
t.wday = 1 << tm.wday;
t.mday = 1 << tm.mday;
t.mon = 1 << (tm.mon + 1);
for(i = 0; i < nuser; i++)
for(j = users[i].jobs; j; j = j->next)
if(j->time.min & t.min
&& j->time.hour & t.hour
&& j->time.wday & t.wday
&& j->time.mday & t.mday
&& j->time.mon & t.mon)
rexec(&users[i], j);
}
seek(lock, 0, 0);
write(lock, "x", 1); /* keep the lock alive */
/*
* if we're not at next minute yet, sleep until a second past
* (to allow for sleep intervals being approximate),
* which synchronises with minute roll-over as a side-effect.
*/
sleepuntil(now + Minute + 1);
}
/* not reached */
}
void
createuser(void)
{
Dir d;
char file[128], *user;
int fd;
user = getuser();
snprint(file, sizeof file, "/cron/%s", user);
fd = create(file, OREAD, 0755|DMDIR);
if(fd < 0)
fatal("couldn't create %s: %r", file);
nulldir(&d);
d.gid = user;
dirfwstat(fd, &d);
close(fd);
snprint(file, sizeof file, "/cron/%s/cron", user);
fd = create(file, OREAD, 0644);
if(fd < 0)
fatal("couldn't create %s: %r", file);
nulldir(&d);
d.gid = user;
dirfwstat(fd, &d);
close(fd);
}
void
readalljobs(void)
{
User *u;
Dir *d, *du;
char file[128];
int i, n, fd;
fd = open("/cron", OREAD);
if(fd < 0)
fatal("can't open /cron: %r");
while((n = dirread(fd, &d)) > 0){
for(i = 0; i < n; i++){
if(strcmp(d[i].name, "log") == 0 ||
!(d[i].qid.type & QTDIR))
continue;
if(strcmp(d[i].name, d[i].uid) != 0){
syslog(1, CRONLOG, "cron for %s owned by %s",
d[i].name, d[i].uid);
continue;
}
u = newuser(d[i].name);
snprint(file, sizeof file, "/cron/%s/cron", d[i].name);
du = dirstat(file);
if(du == nil || qidcmp(u->lastqid, du->qid) != 0){
freejobs(u->jobs);
u->jobs = readjobs(file, u);
}
free(du);
}
free(d);
}
close(fd);
}
/*
* parse user's cron file
* other lines: minute hour monthday month weekday host command
*/
Job *
readjobs(char *file, User *user)
{
Biobuf *b;
Job *j, *jobs;
Dir *d;
int line;
d = dirstat(file);
if(!d)
return nil;
b = Bopen(file, OREAD);
if(!b){
free(d);
return nil;
}
jobs = nil;
user->lastqid = d->qid;
free(d);
for(line = 1; savec = Brdline(b, '\n'); line++){
savec[Blinelen(b) - 1] = '\0';
while(*savec == ' ' || *savec == '\t')
savec++;
if(*savec == '#' || *savec == '\0')
continue;
if(strlen(savec) > 1024){
clog("%s: line %d: line too long", user->name, line);
continue;
}
j = emalloc(sizeof *j);
j->time.min = gettime(0, 59);
if(j->time.min && (j->time.hour = gettime(0, 23))
&& (j->time.mday = gettime(1, 31))
&& (j->time.mon = gettime(1, 12))
&& (j->time.wday = gettime(0, 6))
&& getname(&j->host)){
j->cmd = emalloc(strlen(savec) + 1);
strcpy(j->cmd, savec);
j->next = jobs;
jobs = j;
}else{
clog("%s: line %d: syntax error", user->name, line);
free(j);
}
}
Bterm(b);
return jobs;
}
void
printjobs(void)
{
char buf[8*1024];
Job *j;
int i;
for(i = 0; i < nuser; i++){
print("user %s\n", users[i].name);
for(j = users[i].jobs; j; j = j->next)
if(!mkcmd(j->cmd, buf, sizeof buf))
print("\tbad job %s on host %s\n",
j->cmd, j->host);
else
print("\tjob %s on host %s\n", buf, j->host);
}
}
User *
newuser(char *name)
{
int i;
for(i = 0; i < nuser; i++)
if(strcmp(users[i].name, name) == 0)
return &users[i];
if(nuser == maxuser){
maxuser += 32;
users = erealloc(users, maxuser * sizeof *users);
}
memset(&users[nuser], 0, sizeof(users[nuser]));
users[nuser].name = strdup(name);
users[nuser].jobs = 0;
users[nuser].lastqid.type = QTFILE;
users[nuser].lastqid.path = ~0LL;
users[nuser].lastqid.vers = ~0L;
return &users[nuser++];
}
void
freejobs(Job *j)
{
Job *next;
for(; j; j = next){
next = j->next;
free(j->cmd);
free(j->host);
free(j);
}
}
int
getname(char **namep)
{
int c;
char buf[64], *p;
if(!savec)
return 0;
while(*savec == ' ' || *savec == '\t')
savec++;
for(p = buf; (c = *savec) && c != ' ' && c != '\t'; p++){
if(p >= buf+sizeof buf -1)
return 0;
*p = *savec++;
}
*p = '\0';
*namep = strdup(buf);
if(*namep == 0){
clog("internal error: strdup failure");
_exits(0);
}
while(*savec == ' ' || *savec == '\t')
savec++;
return p > buf;
}
/*
* return the next time range (as a bit vector) in the file:
* times: '*'
* | range
* range: number
* | number '-' number
* | range ',' range
* a return of zero means a syntax error was discovered
*/
uvlong
gettime(int min, int max)
{
uvlong n, m, e;
if(gettok(min, max) == '*')
return ~0ULL;
n = 0;
while(tok == '1'){
m = 1ULL << lexval;
n |= m;
if(gettok(0, 0) == '-'){
if(gettok(lexval, max) != '1')
return 0;
e = 1ULL << lexval;
for( ; m <= e; m <<= 1)
n |= m;
gettok(min, max);
}
if(tok != ',')
break;
if(gettok(min, max) != '1')
return 0;
}
pushtok();
return n;
}
void
pushtok(void)
{
savec = savetok;
}
int
gettok(int min, int max)
{
char c;
savetok = savec;
if(!savec)
return tok = 0;
while((c = *savec) == ' ' || c == '\t')
savec++;
switch(c){
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
lexval = strtoul(savec, &savec, 10);
if(lexval < min || lexval > max)
return tok = 0;
return tok = '1';
case '*': case '-': case ',':
savec++;
return tok = c;
default:
return tok = 0;
}
}
int
call(char *host)
{
char *na, *p;
na = netmkaddr(host, 0, "rexexec");
p = utfrune(na, L'!');
if(!p)
return -1;
p = utfrune(p+1, L'!');
if(!p)
return -1;
if(strcmp(p, "!rexexec") != 0)
return -2;
return dial(na, 0, 0, 0);
}
/*
* convert command to run properly on the remote machine
* need to escape the quotes so they don't get stripped
*/
int
mkcmd(char *cmd, char *buf, int len)
{
char *p;
int n, m;
n = sizeof "exec rc -c '" -1;
if(n >= len)
return 0;
strcpy(buf, "exec rc -c '");
while(p = utfrune(cmd, L'\'')){
p++;
m = p - cmd;
if(n + m + 1 >= len)
return 0;
strncpy(&buf[n], cmd, m);
n += m;
buf[n++] = '\'';
cmd = p;
}
m = strlen(cmd);
if(n + m + sizeof "'</dev/null>/dev/null>[2=1]" >= len)
return 0;
strcpy(&buf[n], cmd);
strcpy(&buf[n+m], "'</dev/null>/dev/null>[2=1]");
return 1;
}
void
rexec(User *user, Job *j)
{
char buf[8*1024];
int n, fd;
AuthInfo *ai;
switch(rfork(RFPROC|RFNOWAIT|RFNAMEG|RFENVG|RFFDG)){
case 0:
break;
case -1:
clog("can't fork a job for %s: %r\n", user->name);
default:
return;
}
if(!mkcmd(j->cmd, buf, sizeof buf)){
clog("internal error: cmd buffer overflow");
_exits(0);
}
/*
* local call, auth, cmd with no i/o
*/
if(strcmp(j->host, "local") == 0){
if(becomeuser(user->name) < 0){
clog("%s: can't change uid for %s on %s: %r",
user->name, j->cmd, j->host);
_exits(0);
}
putenv("service", "rx");
clog("%s: ran '%s' on %s", user->name, j->cmd, j->host);
execl("/bin/rc", "rc", "-lc", buf, nil);
clog("%s: exec failed for %s on %s: %r",
user->name, j->cmd, j->host);
_exits(0);
}
/*
* remote call, auth, cmd with no i/o
* give it 2 min to complete
*/
alarm(2*Minute*1000);
fd = call(j->host);
if(fd < 0){
if(fd == -2)
clog("%s: dangerous host %s", user->name, j->host);
clog("%s: can't call %s: %r", user->name, j->host);
_exits(0);
}
clog("%s: called %s on %s", user->name, j->cmd, j->host);
if(becomeuser(user->name) < 0){
clog("%s: can't change uid for %s on %s: %r",
user->name, j->cmd, j->host);
_exits(0);
}
ai = auth_proxy(fd, nil, "proto=p9any role=client");
if(ai == nil){
clog("%s: can't authenticate for %s on %s: %r",
user->name, j->cmd, j->host);
_exits(0);
}
clog("%s: authenticated %s on %s", user->name, j->cmd, j->host);
write(fd, buf, strlen(buf)+1);
write(fd, buf, 0);
while((n = read(fd, buf, sizeof(buf)-1)) > 0){
buf[n] = 0;
clog("%s: %s\n", j->cmd, buf);
}
_exits(0);
}
void *
emalloc(ulong n)
{
void *p;
if(p = mallocz(n, 1))
return p;
fatal("out of memory");
return 0;
}
void *
erealloc(void *p, ulong n)
{
if(p = realloc(p, n))
return p;
fatal("out of memory");
return 0;
}
void
usage(void)
{
fprint(2, "usage: cron [-c]\n");
exits("usage");
}
int
qidcmp(Qid a, Qid b)
{
/* might be useful to know if a > b, but not for cron */
return(a.path != b.path || a.vers != b.vers);
}
void
memrandom(void *p, int n)
{
uchar *cp;
for(cp = (uchar*)p; n > 0; n--)
*cp++ = fastrand();
}
/*
* keep caphash fd open since opens of it could be disabled
*/
static int caphashfd;
void
initcap(void)
{
caphashfd = open("#¤/caphash", OCEXEC|OWRITE);
if(caphashfd < 0)
fprint(2, "%s: opening #¤/caphash: %r\n", argv0);
}
/*
* create a change uid capability
*/
char*
mkcap(char *from, char *to)
{
uchar rand[20];
char *cap;
char *key;
int nfrom, nto, ncap;
uchar hash[SHA1dlen];
if(caphashfd < 0)
return nil;
/* create the capability */
nto = strlen(to);
nfrom = strlen(from);
ncap = nfrom + 1 + nto + 1 + sizeof(rand)*3 + 1;
cap = emalloc(ncap);
snprint(cap, ncap, "%s@%s", from, to);
memrandom(rand, sizeof(rand));
key = cap+nfrom+1+nto+1;
enc64(key, sizeof(rand)*3, rand, sizeof(rand));
/* hash the capability */
hmac_sha1((uchar*)cap, strlen(cap), (uchar*)key, strlen(key), hash, nil);
/* give the kernel the hash */
key[-1] = '@';
if(write(caphashfd, hash, SHA1dlen) < 0){
free(cap);
return nil;
}
return cap;
}
int
usecap(char *cap)
{
int fd, rv;
fd = open("#¤/capuse", OWRITE);
if(fd < 0)
return -1;
rv = write(fd, cap, strlen(cap));
close(fd);
return rv;
}
int
becomeuser(char *new)
{
char *cap;
int rv;
cap = mkcap(getuser(), new);
if(cap == nil)
return -1;
rv = usecap(cap);
free(cap);
newns(new, nil);
return rv;
}