Subversion Repositories planix.SVN

Rev

Rev 2 | Blame | Compare with Previous | Last modification | View Log | RSS feed

#include <u.h>
#include <libc.h>
#include <bio.h>
#include <libsec.h>
#include <auth.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;
}