Subversion Repositories planix.SVN

Rev

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

#include "common.h"
#include <ctype.h>
#include <auth.h>
#include <libsec.h>

typedef struct Cmd Cmd;
struct Cmd
{
        char *name;
        int needauth;
        int (*f)(char*);
};

static void hello(void);
static int apopcmd(char*);
static int capacmd(char*);
static int delecmd(char*);
static int listcmd(char*);
static int noopcmd(char*);
static int passcmd(char*);
static int quitcmd(char*);
static int rsetcmd(char*);
static int retrcmd(char*);
static int statcmd(char*);
static int stlscmd(char*);
static int topcmd(char*);
static int synccmd(char*);
static int uidlcmd(char*);
static int usercmd(char*);
static char *nextarg(char*);
static int getcrnl(char*, int);
static int readmbox(char*);
static void sendcrnl(char*, ...);
static int senderr(char*, ...);
static int sendok(char*, ...);
#pragma varargck argpos sendcrnl 1
#pragma varargck argpos senderr 1
#pragma varargck argpos sendok 1

Cmd cmdtab[] =
{
        "apop", 0, apopcmd,
        "capa", 0, capacmd,
        "dele", 1, delecmd,
        "list", 1, listcmd,
        "noop", 0, noopcmd,
        "pass", 0, passcmd,
        "quit", 0, quitcmd,
        "rset", 0, rsetcmd,
        "retr", 1, retrcmd,
        "stat", 1, statcmd,
        "stls", 0, stlscmd,
        "sync", 1, synccmd,
        "top", 1, topcmd,
        "uidl", 1, uidlcmd,
        "user", 0, usercmd,
        0, 0, 0,
};

static Biobuf in;
static Biobuf out;
static int passwordinclear;
static int didtls;

typedef struct Msg Msg;
struct Msg
{
        int upasnum;
        char digest[64];
        int bytes;
        int deleted;
};

static int totalbytes;
static int totalmsgs;
static Msg *msg;
static int nmsg;
static int loggedin;
static int debug;
static uchar *tlscert;
static int ntlscert;
static char *peeraddr;
static char tmpaddr[64];

void
usage(void)
{
        fprint(2, "usage: upas/pop3 [-a authmboxfile] [-d debugfile] [-p] "
                "[-r remote] [-t cert]\n");
        exits("usage");
}

void
main(int argc, char **argv)
{
        int fd;
        char *arg, cmdbuf[1024];
        Cmd *c;

        rfork(RFNAMEG);
        Binit(&in, 0, OREAD);
        Binit(&out, 1, OWRITE);

        ARGBEGIN{
        case 'a':
                loggedin = 1;
                if(readmbox(EARGF(usage())) < 0)
                        exits(nil);
                break;
        case 'd':
                debug++;
                if((fd = create(EARGF(usage()), OWRITE, 0666)) >= 0 && fd != 2){
                        dup(fd, 2);
                        close(fd);
                }
                break;
        case 'p':
                passwordinclear = 1;
                break;
        case 'r':
                strecpy(tmpaddr, tmpaddr+sizeof tmpaddr, EARGF(usage()));
                if(arg = strchr(tmpaddr, '!'))
                        *arg = '\0';
                peeraddr = tmpaddr;
                break;
        case 't':
                tlscert = readcert(EARGF(usage()), &ntlscert);
                if(tlscert == nil){
                        senderr("cannot read TLS certificate: %r");
                        exits(nil);
                }
                break;
        }ARGEND

        /* do before TLS */
        if(peeraddr == nil)
                peeraddr = remoteaddr(0,0);

        hello();

        while(Bflush(&out), getcrnl(cmdbuf, sizeof cmdbuf) > 0){
                arg = nextarg(cmdbuf);
                for(c=cmdtab; c->name; c++)
                        if(cistrcmp(c->name, cmdbuf) == 0)
                                break;
                if(c->name == 0){
                        senderr("unknown command %s", cmdbuf);
                        continue;
                }
                if(c->needauth && !loggedin){
                        senderr("%s requires authentication", cmdbuf);
                        continue;
                }
                (*c->f)(arg);
        }
        exits(nil);
}

/* sort directories in increasing message number order */
static int
dircmp(void *a, void *b)
{
        return atoi(((Dir*)a)->name) - atoi(((Dir*)b)->name);
}

static int
readmbox(char *box)
{
        int fd, i, n, nd, lines, pid;
        char buf[100], err[Errlen];
        char *p;
        Biobuf *b;
        Dir *d, *draw;
        Msg *m;
        Waitmsg *w;

        unmount(nil, "/mail/fs");
        switch(pid = fork()){
        case -1:
                return senderr("can't fork to start upas/fs");

        case 0:
                close(0);
                close(1);
                open("/dev/null", OREAD);
                open("/dev/null", OWRITE);
                execl("/bin/upas/fs", "upas/fs", "-np", "-f", box, nil);
                snprint(err, sizeof err, "upas/fs: %r");
                _exits(err);
                break;

        default:
                break;
        }

        if((w = wait()) == nil || w->pid != pid || w->msg[0] != '\0'){
                if(w && w->pid==pid)
                        return senderr("%s", w->msg);
                else
                        return senderr("can't initialize upas/fs");
        }
        free(w);

        if(chdir("/mail/fs/mbox") < 0)
                return senderr("can't initialize upas/fs: %r");

        if((fd = open(".", OREAD)) < 0)
                return senderr("cannot open /mail/fs/mbox: %r");
        nd = dirreadall(fd, &d);
        close(fd);
        if(nd < 0)
                return senderr("cannot read from /mail/fs/mbox: %r");

        msg = mallocz(sizeof(Msg)*nd, 1);
        if(msg == nil)
                return senderr("out of memory");

        if(nd == 0)
                return 0;
        qsort(d, nd, sizeof(d[0]), dircmp);

        for(i=0; i<nd; i++){
                m = &msg[nmsg];
                m->upasnum = atoi(d[i].name);
                sprint(buf, "%d/digest", m->upasnum);
                if((fd = open(buf, OREAD)) < 0)
                        continue;
                n = readn(fd, m->digest, sizeof m->digest - 1);
                close(fd);
                if(n < 0)
                        continue;
                m->digest[n] = '\0';

                /*
                 * We need the number of message lines so that we
                 * can adjust the byte count to include \r's.
                 * Upas/fs gives us the number of lines in the raw body
                 * in the lines file, but we have to count rawheader ourselves.
                 * There is one blank line between raw header and raw body.
                 */
                sprint(buf, "%d/rawheader", m->upasnum);
                if((b = Bopen(buf, OREAD)) == nil)
                        continue;
                lines = 0;
                for(;;){
                        p = Brdline(b, '\n');
                        if(p == nil){
                                if((n = Blinelen(b)) == 0)
                                        break;
                                Bseek(b, n, 1);
                        }else
                                lines++;
                }
                Bterm(b);
                lines++;
                sprint(buf, "%d/lines", m->upasnum);
                if((fd = open(buf, OREAD)) < 0)
                        continue;
                n = readn(fd, buf, sizeof buf - 1);
                close(fd);
                if(n < 0)
                        continue;
                buf[n] = '\0';
                lines += atoi(buf);

                sprint(buf, "%d/raw", m->upasnum);
                if((draw = dirstat(buf)) == nil)
                        continue;
                m->bytes = lines+draw->length;
                free(draw);
                nmsg++;
                totalmsgs++;
                totalbytes += m->bytes;
        }
        return 0;
}

/*
 *  get a line that ends in crnl or cr, turn terminating crnl into a nl
 *
 *  return 0 on EOF
 */
static int
getcrnl(char *buf, int n)
{
        int c;
        char *ep;
        char *bp;
        Biobuf *fp = &in;

        Bflush(&out);

        bp = buf;
        ep = bp + n - 1;
        while(bp != ep){
                c = Bgetc(fp);
                if(debug) {
                        seek(2, 0, 2);
                        fprint(2, "%c", c);
                }
                switch(c){
                case -1:
                        *bp = 0;
                        if(bp==buf)
                                return 0;
                        else
                                return bp-buf;
                case '\r':
                        c = Bgetc(fp);
                        if(c == '\n'){
                                if(debug) {
                                        seek(2, 0, 2);
                                        fprint(2, "%c", c);
                                }
                                *bp = 0;
                                return bp-buf;
                        }
                        Bungetc(fp);
                        c = '\r';
                        break;
                case '\n':
                        *bp = 0;
                        return bp-buf;
                }
                *bp++ = c;
        }
        *bp = 0;
        return bp-buf;
}

static void
sendcrnl(char *fmt, ...)
{
        char buf[1024];
        va_list arg;

        va_start(arg, fmt);
        vseprint(buf, buf+sizeof(buf), fmt, arg);
        va_end(arg);
        if(debug)
                fprint(2, "-> %s\n", buf);
        Bprint(&out, "%s\r\n", buf);
}

static int
senderr(char *fmt, ...)
{
        char buf[1024];
        va_list arg;

        va_start(arg, fmt);
        vseprint(buf, buf+sizeof(buf), fmt, arg);
        va_end(arg);
        if(debug)
                fprint(2, "-> -ERR %s\n", buf);
        Bprint(&out, "-ERR %s\r\n", buf);
        return -1;
}

static int
sendok(char *fmt, ...)
{
        char buf[1024];
        va_list arg;

        va_start(arg, fmt);
        vseprint(buf, buf+sizeof(buf), fmt, arg);
        va_end(arg);
        if(*buf){
                if(debug)
                        fprint(2, "-> +OK %s\n", buf);
                Bprint(&out, "+OK %s\r\n", buf);
        } else {
                if(debug)
                        fprint(2, "-> +OK\n");
                Bprint(&out, "+OK\r\n");
        }
        return 0;
}

static int
capacmd(char*)
{
        sendok("");
        sendcrnl("TOP");
        if(passwordinclear || didtls)
                sendcrnl("USER");
        sendcrnl("PIPELINING");
        sendcrnl("UIDL");
        sendcrnl("STLS");
        sendcrnl(".");
        return 0;
}

static int
delecmd(char *arg)
{
        int n;

        if(*arg==0)
                return senderr("DELE requires a message number");

        n = atoi(arg)-1;
        if(n < 0 || n >= nmsg || msg[n].deleted)
                return senderr("no such message");

        msg[n].deleted = 1;
        totalmsgs--;
        totalbytes -= msg[n].bytes;
        sendok("message %d deleted", n+1);
        return 0;
}

static int
listcmd(char *arg)
{
        int i, n;

        if(*arg == 0){
                sendok("+%d message%s (%d octets)", totalmsgs, totalmsgs==1 ? "":"s", totalbytes);
                for(i=0; i<nmsg; i++){
                        if(msg[i].deleted)
                                continue;
                        sendcrnl("%d %d", i+1, msg[i].bytes);
                }
                sendcrnl(".");
        }else{
                n = atoi(arg)-1;
                if(n < 0 || n >= nmsg || msg[n].deleted)
                        return senderr("no such message");
                sendok("%d %d", n+1, msg[n].bytes);
        }
        return 0;
}

static int
noopcmd(char *arg)
{
        USED(arg);
        sendok("");
        return 0;
}

static void
_synccmd(char*)
{
        int i, fd;
        char *s;
        Fmt f;

        if(!loggedin){
                sendok("");
                return;
        }

        fmtstrinit(&f);
        fmtprint(&f, "delete mbox");
        for(i=0; i<nmsg; i++)
                if(msg[i].deleted)
                        fmtprint(&f, " %d", msg[i].upasnum);
        s = fmtstrflush(&f);
        if(strcmp(s, "delete mbox") != 0){      /* must have something to delete */
                if((fd = open("../ctl", OWRITE)) < 0){
                        senderr("open ctl to delete messages: %r");
                        return;
                }
                if(write(fd, s, strlen(s)) < 0){
                        senderr("error deleting messages: %r");
                        return;
                }
        }
        sendok("");
}

static int
synccmd(char*)
{
        _synccmd(nil);
        return 0;
}

static int
quitcmd(char*)
{
        synccmd(nil);
        exits(nil);
        return 0;
}

static int
retrcmd(char *arg)
{
        int n;
        Biobuf *b;
        char buf[40], *p;

        if(*arg == 0)
                return senderr("RETR requires a message number");
        n = atoi(arg)-1;
        if(n < 0 || n >= nmsg || msg[n].deleted)
                return senderr("no such message");
        snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum);
        if((b = Bopen(buf, OREAD)) == nil)
                return senderr("message disappeared");
        sendok("");
        while((p = Brdstr(b, '\n', 1)) != nil){
                if(p[0]=='.')
                        Bwrite(&out, ".", 1);
                Bwrite(&out, p, strlen(p));
                Bwrite(&out, "\r\n", 2);
                free(p);
        }
        Bterm(b);
        sendcrnl(".");
        return 0;
}

static int
rsetcmd(char*)
{
        int i;

        for(i=0; i<nmsg; i++){
                if(msg[i].deleted){
                        msg[i].deleted = 0;
                        totalmsgs++;
                        totalbytes += msg[i].bytes;
                }
        }
        return sendok("");
}

static int
statcmd(char*)
{
        return sendok("%d %d", totalmsgs, totalbytes);
}

static int
trace(char *fmt, ...)
{
        va_list arg;
        int n;

        va_start(arg, fmt);
        n = vfprint(2, fmt, arg);
        va_end(arg);
        return n;
}

static int
stlscmd(char*)
{
        int fd;
        TLSconn conn;

        if(didtls)
                return senderr("tls already started");
        if(!tlscert)
                return senderr("don't have any tls credentials");
        sendok("");
        Bflush(&out);

        memset(&conn, 0, sizeof conn);
        conn.cert = tlscert;
        conn.certlen = ntlscert;
        if(debug)
                conn.trace = trace;
        fd = tlsServer(0, &conn);
        if(fd < 0)
                sysfatal("tlsServer: %r");
        dup(fd, 0);
        dup(fd, 1);
        close(fd);
        Binit(&in, 0, OREAD);
        Binit(&out, 1, OWRITE);
        didtls = 1;
        return 0;
}

static int
topcmd(char *arg)
{
        int done, i, lines, n;
        char buf[40], *p;
        Biobuf *b;

        if(*arg == 0)
                return senderr("TOP requires a message number");
        n = atoi(arg)-1;
        if(n < 0 || n >= nmsg || msg[n].deleted)
                return senderr("no such message");
        arg = nextarg(arg);
        if(*arg == 0)
                return senderr("TOP requires a line count");
        lines = atoi(arg);
        if(lines < 0)
                return senderr("bad args to TOP");
        snprint(buf, sizeof buf, "%d/raw", msg[n].upasnum);
        if((b = Bopen(buf, OREAD)) == nil)
                return senderr("message disappeared");
        sendok("");
        while(p = Brdstr(b, '\n', 1)){
                if(p[0]=='.')
                        Bputc(&out, '.');
                Bwrite(&out, p, strlen(p));
                Bwrite(&out, "\r\n", 2);
                done = p[0]=='\0';
                free(p);
                if(done)
                        break;
        }
        for(i=0; i<lines; i++){
                p = Brdstr(b, '\n', 1);
                if(p == nil)
                        break;
                if(p[0]=='.')
                        Bwrite(&out, ".", 1);
                Bwrite(&out, p, strlen(p));
                Bwrite(&out, "\r\n", 2);
                free(p);
        }
        sendcrnl(".");
        Bterm(b);
        return 0;
}

static int
uidlcmd(char *arg)
{
        int n;

        if(*arg==0){
                sendok("");
                for(n=0; n<nmsg; n++){
                        if(msg[n].deleted)
                                continue;
                        sendcrnl("%d %s", n+1, msg[n].digest);
                }
                sendcrnl(".");
        }else{
                n = atoi(arg)-1;
                if(n < 0 || n >= nmsg || msg[n].deleted)
                        return senderr("no such message");
                sendok("%d %s", n+1, msg[n].digest);
        }
        return 0;
}

static char*
nextarg(char *p)
{
        while(*p && *p != ' ' && *p != '\t')
                p++;
        while(*p == ' ' || *p == '\t')
                *p++ = 0;
        return p;
}

/*
 * authentication
 */
Chalstate *chs;
char user[256];
char box[256];
char cbox[256];

static void
hello(void)
{
        fmtinstall('H', encodefmt);
        if((chs = auth_challenge("proto=apop role=server")) == nil){
                senderr("auth server not responding, try later");
                exits(nil);
        }

        sendok("POP3 server ready %s", chs->chal);
}

static int
setuser(char *arg)
{
        char *p;

        strcpy(box, "/mail/box/");
        strecpy(box+strlen(box), box+sizeof box-7, arg);
        strcpy(cbox, box);
        cleanname(cbox);
        if(strcmp(cbox, box) != 0)
                return senderr("bad mailbox name");
        strcat(box, "/mbox");

        strecpy(user, user+sizeof user, arg);
        if(p = strchr(user, '/'))
                *p = '\0';
        return 0;
}

static int
usercmd(char *arg)
{
        if(loggedin)
                return senderr("already authenticated");
        if(*arg == 0)
                return senderr("USER requires argument");
        if(setuser(arg) < 0)
                return -1;
        return sendok("");
}

static void
enableaddr(void)
{
        int fd;
        char buf[64];

        /* hide the peer IP address under a rock in the ratifier FS */
        if(peeraddr == 0 || *peeraddr == 0)
                return;

        sprint(buf, "/mail/ratify/trusted/%s#32", peeraddr);

        /*
         * if the address is already there and the user owns it,
         * remove it and recreate it to give him a new time quanta.
         */
        if(access(buf, 0) >= 0  && remove(buf) < 0)
                return;

        fd = create(buf, OREAD, 0666);
        if(fd >= 0){
                close(fd);
//              syslog(0, "pop3", "ratified %s", peeraddr);
        }
}

static int
dologin(char *response)
{
        AuthInfo *ai;
        static int tries;
        static ulong delaysecs = 5;

        chs->user = user;
        chs->resp = response;
        chs->nresp = strlen(response);
        if((ai = auth_response(chs)) == nil){
                if(tries >= 20){
                        senderr("authentication failed: %r; server exiting");
                        exits(nil);
                }
                if(++tries == 3)
                        syslog(0, "pop3", "likely password guesser from %s",
                                peeraddr);
                delaysecs *= 2;
                if (delaysecs > 30*60)
                        delaysecs = 30*60;              /* half-hour max. */
                sleep(delaysecs * 1000); /* prevent beating on our auth server */
                return senderr("authentication failed");
        }

        if(auth_chuid(ai, nil) < 0){
                senderr("chuid failed: %r; server exiting");
                exits(nil);
        }
        auth_freeAI(ai);
        auth_freechal(chs);
        chs = nil;

        loggedin = 1;
        if(newns(user, 0) < 0){
                senderr("newns failed: %r; server exiting");
                exits(nil);
        }
        syslog(0, "pop3", "user %s logged in", user);
        enableaddr();
        if(readmbox(box) < 0)
                exits(nil);
        return sendok("mailbox is %s", box);
}

static int
passcmd(char *arg)
{
        DigestState *s;
        uchar digest[MD5dlen];
        char response[2*MD5dlen+1];

        if(passwordinclear==0 && didtls==0)
                return senderr("password in the clear disallowed");

        /* use password to encode challenge */
        if((chs = auth_challenge("proto=apop role=server")) == nil)
                return senderr("couldn't get apop challenge");

        /* hash challenge with secret and convert to ascii */
        s = md5((uchar*)chs->chal, chs->nchal, 0, 0);
        md5((uchar*)arg, strlen(arg), digest, s);
        snprint(response, sizeof response, "%.*H", MD5dlen, digest);
        return dologin(response);
}

static int
apopcmd(char *arg)
{
        char *resp;

        resp = nextarg(arg);
        if(setuser(arg) < 0)
                return -1;
        return dologin(resp);
}