Subversion Repositories planix.SVN

Rev

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

#include <u.h>
#include <libc.h>
#include <auth.h>
#include <mp.h>
#include <libsec.h>
#include "httpd.h"
#include "httpsrv.h"

enum {
        Nbuckets        = 256,
};

typedef struct Strings          Strings;
typedef struct System           System;

struct Strings
{
        char    *s1;
        char    *s2;
};
struct System {
        char    *rsys;
        ulong   reqs;
        ulong   first;
        ulong   last;
        System  *next;                  /* next in chain */
};

char    *netdir;
char    *HTTPLOG = "httpd/log";

static  char            netdirb[256];
static  char            *namespace;
static  System          syss[Nbuckets];

static  void            becomenone(char*);
static  char            *csquery(char*, char*, char*);
static  void            dolisten(char*);
static  int             doreq(HConnect*);
static  int             send(HConnect*);
static  Strings         stripmagic(HConnect*, char*);
static  char*           stripprefix(char*, char*);
static  char*           sysdom(void);
static  int             notfound(HConnect *c, char *url);

uchar *certificate;
int certlen;
PEMChain *certchain;    

void
usage(void)
{
        fprint(2, "usage: httpd [-c certificate] [-C CAchain] [-a srvaddress] "
                "[-d domain] [-n namespace] [-w webroot]\n");
        exits("usage");
}

void
main(int argc, char **argv)
{
        char *address;

        namespace = nil;
        address = nil;
        hmydomain = nil;
        netdir = "/net";
        fmtinstall('D', hdatefmt);
        fmtinstall('H', httpfmt);
        fmtinstall('U', hurlfmt);
        ARGBEGIN{
        case 'c':
                certificate = readcert(EARGF(usage()), &certlen);
                if(certificate == nil)
                        sysfatal("reading certificate: %r");
                break;
        case 'C':
                certchain = readcertchain(EARGF(usage()));
                if (certchain == nil)
                        sysfatal("reading certificate chain: %r");
                break;
        case 'n':
                namespace = EARGF(usage());
                break;
        case 'a':
                address = EARGF(usage());
                break;
        case 'd':
                hmydomain = EARGF(usage());
                break;
        case 'w':
                webroot = EARGF(usage());
                break;
        default:
                usage();
                break;
        }ARGEND

        if(argc)
                usage();

        if(namespace == nil)
                namespace = "/lib/namespace.httpd";
        if(address == nil)
                address = "*";
        if(webroot == nil)
                webroot = "/usr/web";
        else{
                cleanname(webroot);
                if(webroot[0] != '/')
                        webroot = "/usr/web";
        }

        switch(rfork(RFNOTEG|RFPROC|RFFDG|RFNAMEG)) {
        case -1:
                sysfatal("fork");
        case 0:
                break;
        default:
                exits(nil);
        }

        /*
         * open all files we might need before castrating namespace
         */
        time(nil);
        if(hmydomain == nil)
                hmydomain = sysdom();
        syslog(0, HTTPLOG, nil);
        logall[0] = open("/sys/log/httpd/0", OWRITE);
        logall[1] = open("/sys/log/httpd/1", OWRITE);
        logall[2] = open("/sys/log/httpd/clf", OWRITE);
        redirectinit();
        contentinit();
        urlinit();
        statsinit();

        becomenone(namespace);
        dolisten(netmkaddr(address, "tcp", certificate == nil ? "http" : "https"));
        exits(nil);
}

static void
becomenone(char *namespace)
{
        int fd;

        fd = open("#c/user", OWRITE);
        if(fd < 0 || write(fd, "none", strlen("none")) < 0)
                sysfatal("can't become none");
        close(fd);
        if(newns("none", nil) < 0)
                sysfatal("can't build normal namespace");
        if(addns("none", namespace) < 0)
                sysfatal("can't build httpd namespace");
}

static HConnect*
mkconnect(char *scheme, char *port)
{
        HConnect *c;

        c = ezalloc(sizeof(HConnect));
        c->hpos = c->header;
        c->hstop = c->header;
        c->replog = writelog;
        c->scheme = scheme;
        c->port = port;
        return c;
}

static HSPriv*
mkhspriv(void)
{
        HSPriv *p;

        p = ezalloc(sizeof(HSPriv));
        return p;
}

static uint 
hashstr(char* key)
{
        /* asu works better than pjw for urls */
        uchar *k = (unsigned char*)key;
        uint h = 0;

        while(*k!=0)
                h = 65599*h + *k++;
        return h;
}

static System *
hashsys(char *rsys)
{
        int notme;
        System *sys;

        sys = syss + hashstr(rsys) % nelem(syss);
        /* if the bucket is empty, just use it, else find or allocate ours */
        if(sys->rsys != nil) {
                /* find match or chain end */
                for(; notme = (strcmp(sys->rsys, rsys) != 0) &&
                    sys->next != nil; sys = sys->next)
                        ;
                if(notme) {
                        sys->next = malloc(sizeof *sys);  /* extend chain */
                        sys = sys->next;
                } else
                        return sys;
        }
        if(sys != nil) {
                memset(sys, 0, sizeof *sys);
                sys->rsys = strdup(rsys);
        }
        return sys;
}

/*
 * be sure to call this at least once per listen in the parent,
 * to update the hash chains.
 * it's okay to call it in the child too, but then sys->reqs only gets
 * updated in the child.
 */
static int
isswamped(char *rsys)
{
        ulong period;
        System *sys = hashsys(rsys);

        if(sys == nil)
                return 0;
        sys->last = time(nil);
        if(sys->first == 0)
                sys->first = sys->last;
        period = sys->first - sys->last;
        return ++sys->reqs > 30 && period > 30 && sys->reqs / period >= 2;
}

/* must only be called in child */
static void
throttle(int nctl, NetConnInfo *nci, int swamped)
{
        if(swamped || isswamped(nci->rsys)) {           /* shed load */
                syslog(0, HTTPLOG, "overloaded by %s", nci->rsys);
                sleep(30);
                close(nctl);
                exits(nil);
        }
}

static void
dolisten(char *address)
{
        HSPriv *hp;
        HConnect *c;
        NetConnInfo *nci;
        char ndir[NETPATHLEN], dir[NETPATHLEN], *p, *scheme;
        int ctl, nctl, data, t, ok, spotchk, swamped;
        TLSconn conn;

        spotchk = 0;
        syslog(0, HTTPLOG, "httpd starting");
        ctl = announce(address, dir);
        if(ctl < 0){
                syslog(0, HTTPLOG, "can't announce on %s: %r", address);
                return;
        }
        strcpy(netdirb, dir);
        p = nil;
        if(netdir[0] == '/'){
                p = strchr(netdirb+1, '/');
                if(p != nil)
                        *p = '\0';
        }
        if(p == nil)
                strcpy(netdirb, "/net");
        netdir = netdirb;

        for(;;){

                /*
                 *  wait for a call (or an error)
                 */
                nctl = listen(dir, ndir);
                if(nctl < 0){
                        syslog(0, HTTPLOG, "can't listen on %s: %r", address);
                        syslog(0, HTTPLOG, "ctls = %d", ctl);
                        return;
                }
                swamped = 0;
                nci = getnetconninfo(ndir, -1);
                if (nci)
                        swamped = isswamped(nci->rsys);

                /*
                 *  start a process for the service
                 */
                switch(rfork(RFFDG|RFPROC|RFNOWAIT|RFNAMEG)){
                case -1:
                        close(nctl);
                        continue;
                case 0:
                        /*
                         *  see if we know the service requested
                         */
                        data = accept(ctl, ndir);
                        if(data >= 0 && certificate != nil){
                                memset(&conn, 0, sizeof(conn));
                                conn.cert = certificate;
                                conn.certlen = certlen;
                                if (certchain != nil)
                                        conn.chain = certchain;
                                data = tlsServer(data, &conn);
                                scheme = "https";
                        }else
                                scheme = "http";
                        if(data < 0){
                                syslog(0, HTTPLOG, "can't open %s/data: %r", ndir);
                                exits(nil);
                        }
                        dup(data, 0);
                        dup(data, 1);
                        dup(data, 2);
                        close(data);
                        close(ctl);
                        close(nctl);

                        if (nci == nil)
                                nci = getnetconninfo(ndir, -1);
                        c = mkconnect(scheme, nci->lserv);
                        hp = mkhspriv();
                        hp->remotesys = nci->rsys;
                        hp->remoteserv = nci->rserv;
                        c->private = hp;

                        hinit(&c->hin, 0, Hread);
                        hinit(&c->hout, 1, Hwrite);

                        /*
                         * serve requests until a magic request.
                         * later requests have to come quickly.
                         * only works for http/1.1 or later.
                         */
                        for(t = 15*60*1000; ; t = 15*1000){
                                throttle(nctl, nci, swamped);
                                if(hparsereq(c, t) <= 0)
                                        exits(nil);
                                ok = doreq(c);

                                hflush(&c->hout);

                                if(c->head.closeit || ok < 0)
                                        exits(nil);

                                hreqcleanup(c);
                        }
                        /* not reached */

                default:
                        close(nctl);
                        break;
                }

                if(++spotchk > 50){
                        spotchk = 0;
                        redirectinit();
                        contentinit();
                        urlinit();
                        statsinit();
                }
        }
}

static int
doreq(HConnect *c)
{
        HSPriv *hp;
        Strings ss;
        char *magic, *uri, *newuri, *origuri, *newpath, *hb;
        char virtualhost[100], logfd0[10], logfd1[10], vers[16];
        int n, nredirect;
        uint flags;

        /*
         * munge uri for magic
         */
        uri = c->req.uri;
        nredirect = 0;
        werrstr("");
top:
        if(++nredirect > 10){
                if(hparseheaders(c, 15*60*1000) < 0)
                        exits("failed");
                werrstr("redirection loop");
                return hfail(c, HNotFound, uri);
        }
        ss = stripmagic(c, uri);
        uri = ss.s1;
        origuri = uri;
        magic = ss.s2;
        if(magic)
                goto magic;

        /*
         * Apply redirects.  Do this before reading headers
         * (if possible) so that we can redirect to magic invisibly.
         */
        flags = 0;
        if(origuri[0]=='/' && origuri[1]=='~'){
                n = strlen(origuri) + 4 + UTFmax;
                newpath = halloc(c, n);
                snprint(newpath, n, "/who/%s", origuri+2);
                c->req.uri = newpath;
                newuri = newpath;
        }else if(origuri[0]=='/' && origuri[1]==0){
                /* can't redirect / until we read the headers below */
                newuri = nil;
        }else
                newuri = redirect(c, origuri, &flags);

        if(newuri != nil){
                if(flags & Redirsilent) {
                        c->req.uri = uri = newuri;
                        logit(c, "%s: silent replacement %s", origuri, uri);
                        goto top;
                }
                if(hparseheaders(c, 15*60*1000) < 0)
                        exits("failed");
                if(flags & Redirperm) {
                        logit(c, "%s: permanently moved to %s", origuri, newuri);
                        return hmoved(c, newuri);
                } else if (flags & (Redironly | Redirsubord))
                        logit(c, "%s: top-level or many-to-one replacement %s",
                                origuri, uri);

                /*
                 * try temporary redirect instead of permanent
                 */
                if (http11(c))
                        return hredirected(c, "307 Temporary Redirect", newuri);
                else
                        return hredirected(c, "302 Temporary Redirect", newuri);
        }

        /*
         * for magic we exec a new program and serve no more requests
         */
magic:
        if(magic != nil && strcmp(magic, "httpd") != 0){
                snprint(c->xferbuf, HBufSize, "/bin/ip/httpd/%s", magic);
                snprint(logfd0, sizeof(logfd0), "%d", logall[0]);
                snprint(logfd1, sizeof(logfd1), "%d", logall[1]);
                snprint(vers, sizeof(vers), "HTTP/%d.%d", c->req.vermaj, c->req.vermin);
                hb = hunload(&c->hin);
                if(hb == nil){
                        hfail(c, HInternal);
                        return -1;
                }
                hp = c->private;
                execl(c->xferbuf, magic, "-d", hmydomain, "-w", webroot,
                        "-s", c->scheme, "-p", c->port,
                        "-r", hp->remotesys, "-N", netdir, "-b", hb,
                        "-L", logfd0, logfd1, "-R", c->header,
                        c->req.meth, vers, uri, c->req.search, nil);
                logit(c, "no magic %s uri %s", magic, uri);
                hfail(c, HNotFound, uri);
                return -1;
        }

        /*
         * normal case is just file transfer
         */
        if(hparseheaders(c, 15*60*1000) < 0)
                exits("failed");
        if(origuri[0] == '/' && origuri[1] == 0){       
                snprint(virtualhost, sizeof virtualhost, "http://%s/", c->head.host);
                newuri = redirect(c, virtualhost, nil);
                if(newuri == nil)
                        newuri = redirect(c, origuri, nil);
                if(newuri)
                        return hmoved(c, newuri);
        }
        if(!http11(c) && !c->head.persist)
                c->head.closeit = 1;
        return send(c);
}

static int
send(HConnect *c)
{
        Dir *dir;
        char *w, *w2, *p, *masque;
        int fd, fd1, n, force301, ok;

/*
        if(c->req.search)
                return hfail(c, HNoSearch, c->req.uri);
 */
        if(strcmp(c->req.meth, "GET") != 0 && strcmp(c->req.meth, "HEAD") != 0)
                return hunallowed(c, "GET, HEAD");
        if(c->head.expectother || c->head.expectcont)
                return hfail(c, HExpectFail);

        masque = masquerade(c->head.host);

        /*
         * check for directory/file mismatch with trailing /,
         * and send any redirections.
         */
        n = strlen(webroot) + strlen(masque) + strlen(c->req.uri) +
                STRLEN("/index.html") + STRLEN("/.httplogin") + 1;
        w = halloc(c, n);
        strcpy(w, webroot);
        strcat(w, masque);
        strcat(w, c->req.uri);

        /*
         *  favicon can be overridden by hostname.ico
         */
        if(strcmp(c->req.uri, "/favicon.ico") == 0){
                w2 = halloc(c, n+strlen(c->head.host)+2);
                strcpy(w2, webroot);
                strcat(w2, masque);
                strcat(w2, "/");
                strcat(w2, c->head.host);
                strcat(w2, ".ico");
                if(access(w2, AREAD)==0)
                        w = w2;
        }

        /*
         * don't show the contents of .httplogin
         */
        n = strlen(w);
        if(strcmp(w+n-STRLEN(".httplogin"), ".httplogin") == 0)
                return notfound(c, c->req.uri);

        fd = open(w, OREAD);
        if(fd < 0 && strlen(masque)>0 && strncmp(c->req.uri, masque, strlen(masque)) == 0){
                // may be a URI from before virtual hosts;  try again without masque
                strcpy(w, webroot);
                strcat(w, c->req.uri);
                fd = open(w, OREAD);
        }
        if(fd < 0)
                return notfound(c, c->req.uri);
        dir = dirfstat(fd);
        if(dir == nil){
                close(fd);
                return hfail(c, HInternal);
        }
        p = strchr(w, '\0');
        if(dir->mode & DMDIR){
                free(dir);
                if(p > w && p[-1] == '/'){
                        strcat(w, "index.html");
                        force301 = 0;
                }else{
                        strcat(w, "/index.html");
                        force301 = 1;
                }
                fd1 = open(w, OREAD);
                if(fd1 < 0){
                        close(fd);
                        return notfound(c, c->req.uri);
                }
                c->req.uri = w + strlen(webroot) + strlen(masque);
                if(force301 && c->req.vermaj){
                        close(fd);
                        close(fd1);
                        return hmoved(c, c->req.uri);
                }
                close(fd);
                fd = fd1;
                dir = dirfstat(fd);
                if(dir == nil){
                        close(fd);
                        return hfail(c, HInternal);
                }
        }else if(p > w && p[-1] == '/'){
                free(dir);
                close(fd);
                *strrchr(c->req.uri, '/') = '\0';
                return hmoved(c, c->req.uri);
        }

        ok = authorize(c, w);
        if(ok <= 0){
                free(dir);
                close(fd);
                return ok;
        }

        return sendfd(c, fd, dir, nil, nil);
}

static Strings
stripmagic(HConnect *hc, char *uri)
{
        Strings ss;
        char *newuri, *prog, *s;

        prog = stripprefix("/magic/", uri);
        if(prog == nil){
                ss.s1 = uri;
                ss.s2 = nil;
                return ss;
        }

        s = strchr(prog, '/');
        if(s == nil)
                newuri = "";
        else{
                newuri = hstrdup(hc, s);
                *s = 0;
                s = strrchr(s, '/');
                if(s != nil && s[1] == 0)
                        *s = 0;
        }
        ss.s1 = newuri;
        ss.s2 = prog;
        return ss;
}

static char*
stripprefix(char *pre, char *str)
{
        while(*pre)
                if(*str++ != *pre++)
                        return nil;
        return str;
}

/*
 * couldn't open a file
 * figure out why and return and error message
 */
static int
notfound(HConnect *c, char *url)
{
        c->xferbuf[0] = 0;
        rerrstr(c->xferbuf, sizeof c->xferbuf);
        if(strstr(c->xferbuf, "file does not exist") != nil)
                return hfail(c, HNotFound, url);
        if(strstr(c->xferbuf, "permission denied") != nil)
                return hfail(c, HUnauth, url);
        return hfail(c, HNotFound, url);
}

static char*
sysdom(void)
{
        char *dn;

        dn = csquery("sys" , sysname(), "dom");
        if(dn == nil)
                dn = "who cares";
        return dn;
}

/*
 *  query the connection server
 */
static char*
csquery(char *attr, char *val, char *rattr)
{
        char token[64+4];
        char buf[256], *p, *sp;
        int fd, n;

        if(val == nil || val[0] == 0)
                return nil;
        snprint(buf, sizeof(buf), "%s/cs", netdir);
        fd = open(buf, ORDWR);
        if(fd < 0)
                return nil;
        fprint(fd, "!%s=%s", attr, val);
        seek(fd, 0, 0);
        snprint(token, sizeof(token), "%s=", rattr);
        for(;;){
                n = read(fd, buf, sizeof(buf)-1);
                if(n <= 0)
                        break;
                buf[n] = 0;
                p = strstr(buf, token);
                if(p != nil && (p == buf || *(p-1) == 0)){
                        close(fd);
                        sp = strchr(p, ' ');
                        if(sp)
                                *sp = 0;
                        p = strchr(p, '=');
                        if(p == nil)
                                return nil;
                        return estrdup(p+1);
                }
        }
        close(fd);
        return nil;
}