diff options
author | Taru Karttunen <taruti@taruti.net> | 2011-03-30 15:46:40 +0300 |
---|---|---|
committer | Taru Karttunen <taruti@taruti.net> | 2011-03-30 15:46:40 +0300 |
commit | e5888a1ffdae813d7575f5fb02275c6bb07e5199 (patch) | |
tree | d8d51eac403f07814b9e936eed0c9a79195e2450 /sys/src/cmd/ip/httpd |
Import sources from 2011-03-30 iso image
Diffstat (limited to 'sys/src/cmd/ip/httpd')
-rwxr-xr-x | sys/src/cmd/ip/httpd/anonymous.c | 14 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/authorize.c | 116 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/classify.c | 427 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/content.c | 172 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/emem.c | 25 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/hints.c | 297 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/httpd.c | 609 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/httpsrv.h | 79 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/imagemap.c | 320 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/init.c | 113 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/log.c | 104 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/man2html.c | 449 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/mkfile | 79 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/netlib_find.c | 278 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/netlib_history.c | 218 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/redirect.c | 227 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/save.c | 154 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/sendfd.c | 462 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/webls.c | 335 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/webls.denied | 1 | ||||
-rwxr-xr-x | sys/src/cmd/ip/httpd/wikipost.c | 305 |
21 files changed, 4784 insertions, 0 deletions
diff --git a/sys/src/cmd/ip/httpd/anonymous.c b/sys/src/cmd/ip/httpd/anonymous.c new file mode 100755 index 000000000..1213cf4ce --- /dev/null +++ b/sys/src/cmd/ip/httpd/anonymous.c @@ -0,0 +1,14 @@ +#include <u.h> +#include <libc.h> +#include "httpd.h" +#include "httpsrv.h" + +void +anonymous(HConnect *c) +{ + if(bind(webroot, "/", MREPL) < 0){ + hfail(c, HInternal); + exits(nil); + } + chdir("/"); +} diff --git a/sys/src/cmd/ip/httpd/authorize.c b/sys/src/cmd/ip/httpd/authorize.c new file mode 100755 index 000000000..031945434 --- /dev/null +++ b/sys/src/cmd/ip/httpd/authorize.c @@ -0,0 +1,116 @@ +#include <u.h> +#include <libc.h> +#include <auth.h> +#include "httpd.h" +#include "httpsrv.h" + +static char* readfile(char*); + +/* + * these should be done better; see the response codes in /lib/rfc/rfc2616 for + * more info on what should be included. + */ +#define UNAUTHED "You are not authorized to see this area.\n" + +/* + * check for authorization for some parts of the server tree. + * the user name supplied with the authorization request is ignored; + * instead, we authenticate as the realm's user. + * + * authorization should be done before opening any files so that + * unauthorized users don't get to validate file names. + * + * returns 1 if authorized, 0 if unauthorized, -1 for io failure. + */ +int +authorize(HConnect *c, char *file) +{ + char *p, *p0; + Hio *hout; + char *buf; + int i, n; + char *t[257]; + + p0 = halloc(c, strlen(file)+STRLEN("/.httplogin")+1); + strcpy(p0, file); + for(;;){ + p = strrchr(p0, '/'); + if(p == nil) + return hfail(c, HInternal); + if(*(p+1) != 0) + break; + + /* ignore trailing '/'s */ + *p = 0; + } + strcpy(p, "/.httplogin"); + + buf = readfile(p0); + if(buf == nil){ + return 1; + } + n = tokenize(buf, t, nelem(t)); + + if(c->head.authuser != nil && c->head.authpass != 0){ + for(i = 1; i+1 < n; i += 2){ + if(strcmp(t[i], c->head.authuser) == 0 + && strcmp(t[i+1], c->head.authpass) == 0){ + free(buf); + return 1; + } + } + } + + hout = &c->hout; + hprint(hout, "%s 401 Unauthorized\r\n", hversion); + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "WWW-Authenticate: Basic realm=\"%s\"\r\n", t[0]); + hprint(hout, "Content-Type: text/html\r\n"); + hprint(hout, "Content-Length: %d\r\n", STRLEN(UNAUTHED)); + if(c->head.closeit) + hprint(hout, "Connection: close\r\n"); + else if(!http11(c)) + hprint(hout, "Connection: Keep-Alive\r\n"); + hprint(hout, "\r\n"); + if(strcmp(c->req.meth, "HEAD") != 0) + hprint(hout, "%s", UNAUTHED); + writelog(c, "Reply: 401 Unauthorized\n"); + free(buf); + return hflush(hout); +} + +static char* +readfile(char *file) +{ + Dir *d; + int fd; + char *buf; + int n, len; + + fd = open(file, OREAD); + if(fd < 0) + return nil; + d = dirfstat(fd); + if(d == nil){ /* shouldn't happen */ + close(fd); + return nil; + } + len = d->length; + free(d); + + buf = malloc(len+1); + if(buf == 0){ + close(fd); + return nil; + } + + n = readn(fd, buf, len); + close(fd); + if(n <= 0){ + free(buf); + return nil; + } + buf[n] = '\0'; + return buf; +} diff --git a/sys/src/cmd/ip/httpd/classify.c b/sys/src/cmd/ip/httpd/classify.c new file mode 100755 index 000000000..147d1c285 --- /dev/null +++ b/sys/src/cmd/ip/httpd/classify.c @@ -0,0 +1,427 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include <ndb.h> +#include "whois.h" + +typedef struct Country Country; + +struct Country +{ + char *code; + char *name; +}; + +Country badc[] = +{ + {"af", "afghanistan"}, + {"cu", "cuba"}, + {"ir", "iran"}, + {"iq", "iraq"}, + {"ly", "libya"}, + {"kp", "north korea"}, + {"sd", "sudan"}, + {"sy", "syria"}, + { 0, 0 } +}; + +Country goodc[] = +{ + // the original, us and canada + {"us", "united states of america"}, + {"ca", "canada"}, + {"gov", "gov"}, + {"mil", "mil"}, + + // the european union + { "eu", "european union" }, + { "be", "belgium" }, + { "de", "germany" }, + { "fr", "france" }, + { "it", "italy" }, + { "lu", "luxembourg" }, + { "nl", "netherlands" }, + { "dk", "denmark" }, + { "ie", "ireland" }, + { "gb", "great britain" }, + { "uk", "united kingdom" }, + { "gr", "greece" }, + { "es", "spain" }, + { "pt", "portugal" }, + { "au", "australia" }, + { "fi", "finland" }, + { "se", "sweden" }, + + // the rest + {"au", "australia"}, + {"no", "norway"}, + {"cz", "czech republic"}, + {"hu", "hungary"}, + {"pl", "poland"}, + {"jp", "japan"}, + {"ch", "switzerland"}, + {"nz", "new zealand"}, + { 0, 0 } +}; + +char *gov[] = +{ + "gov", + "gouv", + "mil", + "government", + 0, +}; + +Country allc[] = +{ + { "ad", "andorra" }, + { "ae", "united arab emirates" }, + { "af", "afghanistan" }, + { "ag", "antigua and barbuda" }, + { "ai", "anguilla" }, + { "al", "albania" }, + { "am", "armenia" }, + { "an", "netherlands antilles" }, + { "ao", "angola" }, + { "aq", "antarctica" }, + { "ar", "argentina" }, + { "as", "american samoa" }, + { "at", "austria" }, + { "au", "australia" }, + { "aw", "aruba" }, + { "az", "azerbaijan" }, + { "ba", "bosnia and herzegovina" }, + { "bb", "barbados" }, + { "bd", "bangladesh" }, + { "be", "belgium" }, + { "bf", "burkina faso" }, + { "bg", "bulgaria" }, + { "bh", "bahrain" }, + { "bi", "burundi" }, + { "bj", "benin" }, + { "bm", "bermuda" }, + { "bn", "brunei darussalam" }, + { "bo", "bolivia" }, + { "br", "brazil" }, + { "bs", "bahamas" }, + { "bt", "bhutan" }, + { "bu", "burma" }, + { "bv", "bouvet island" }, + { "bw", "botswana" }, + { "by", "belarus" }, + { "bz", "belize" }, + { "ca", "canada" }, + { "cc", "cocos (keeling) islands" }, + { "cf", "central african republic" }, + { "cg", "congo" }, + { "ch", "switzerland" }, + { "ci", "cote d'ivoire (ivory coast)" }, + { "ck", "cook islands" }, + { "cl", "chile" }, + { "cm", "cameroon" }, + { "cn", "china" }, + { "co", "colombia" }, + { "cr", "costa rica" }, + { "cs", "czechoslovakia (former)" }, + { "ct", "canton and enderbury island" }, + { "cu", "cuba" }, + { "cv", "cape verde" }, + { "cx", "christmas island" }, + { "cy", "cyprus" }, + { "cz", "czech republic" }, + { "dd", "german democratic republic" }, + { "de", "germany" }, + { "dj", "djibouti" }, + { "dk", "denmark" }, + { "dm", "dominica" }, + { "do", "dominican republic" }, + { "dz", "algeria" }, + { "ec", "ecuador" }, + { "ee", "estonia" }, + { "eg", "egypt" }, + { "eh", "western sahara" }, + { "er", "eritrea" }, + { "es", "spain" }, + { "et", "ethiopia" }, + { "eu", "european union" }, + { "fi", "finland" }, + { "fj", "fiji" }, + { "fk", "falkland islands (malvinas)" }, + { "fm", "micronesia" }, + { "fo", "faroe islands" }, + { "fr", "france" }, + { "fx", "france, metropolitan" }, + { "ga", "gabon" }, + { "gb", "great britain (uk)" }, + { "gd", "grenada" }, + { "ge", "georgia" }, + { "gf", "french guiana" }, + { "gh", "ghana" }, + { "gi", "gibraltar" }, + { "gl", "greenland" }, + { "gm", "gambia" }, + { "gn", "guinea" }, + { "gp", "guadeloupe" }, + { "gq", "equatorial guinea" }, + { "gr", "greece" }, + { "gs", "s. georgia and s. sandwich isls." }, + { "gt", "guatemala" }, + { "gu", "guam" }, + { "gw", "guinea-bissau" }, + { "gy", "guyana" }, + { "hk", "hong kong" }, + { "hm", "heard and mcdonald islands" }, + { "hn", "honduras" }, + { "hr", "croatia (hrvatska)" }, + { "ht", "haiti" }, + { "hu", "hungary" }, + { "id", "indonesia" }, + { "ie", "ireland" }, + { "il", "israel" }, + { "in", "india" }, + { "io", "british indian ocean territory" }, + { "iq", "iraq" }, + { "ir", "iran" }, + { "is", "iceland" }, + { "it", "italy" }, + { "jm", "jamaica" }, + { "jo", "jordan" }, + { "jp", "japan" }, + { "jt", "johnston island" }, + { "ke", "kenya" }, + { "kg", "kyrgyzstan" }, + { "kh", "cambodia (democratic kampuchea)" }, + { "ki", "kiribati" }, + { "km", "comoros" }, + { "kn", "saint kitts and nevis" }, + { "kp", "korea (north)" }, + { "kr", "korea (south)" }, + { "kw", "kuwait" }, + { "ky", "cayman islands" }, + { "kz", "kazakhstan" }, + { "la", "laos" }, + { "lb", "lebanon" }, + { "lc", "saint lucia" }, + { "li", "liechtenstein" }, + { "lk", "sri lanka" }, + { "lr", "liberia" }, + { "ls", "lesotho" }, + { "lt", "lithuania" }, + { "lu", "luxembourg" }, + { "lv", "latvia" }, + { "ly", "libya" }, + { "ma", "morocco" }, + { "mc", "monaco" }, + { "md", "moldova" }, + { "mg", "madagascar" }, + { "mh", "marshall islands" }, + { "mi", "midway islands" }, + { "mk", "macedonia" }, + { "ml", "mali" }, + { "mm", "myanmar" }, + { "mn", "mongolia" }, + { "mo", "macau" }, + { "mp", "northern mariana islands" }, + { "mq", "martinique" }, + { "mr", "mauritania" }, + { "ms", "montserrat" }, + { "mt", "malta" }, + { "mu", "mauritius" }, + { "mv", "maldives" }, + { "mw", "malawi" }, + { "mx", "mexico" }, + { "my", "malaysia" }, + { "mz", "mozambique" }, + { "na", "namibia" }, + { "nc", "new caledonia" }, + { "ne", "niger" }, + { "nf", "norfolk island" }, + { "ng", "nigeria" }, + { "ni", "nicaragua" }, + { "nl", "netherlands" }, + { "no", "norway" }, + { "np", "nepal" }, + { "nq", "dronning maud land" }, + { "nr", "nauru" }, + { "nt", "neutral zone" }, + { "nu", "niue" }, + { "nz", "new zealand (aotearoa)" }, + { "om", "oman" }, + { "pa", "panama" }, + { "pc", "pacific islands" }, + { "pe", "peru" }, + { "pf", "french polynesia" }, + { "pg", "papua new guinea" }, + { "ph", "philippines" }, + { "pk", "pakistan" }, + { "pl", "poland" }, + { "pm", "st. pierre and miquelon" }, + { "pn", "pitcairn" }, + { "pr", "puerto rico" }, + { "pu", "united states misc. pacific islands" }, + { "pt", "portugal" }, + { "pw", "palau" }, + { "py", "paraguay" }, + { "qa", "qatar" }, + { "re", "reunion" }, + { "ro", "romania" }, + { "ru", "russian federation" }, + { "rw", "rwanda" }, + { "sa", "saudi arabia" }, + { "sb", "solomon islands" }, + { "sc", "seychelles" }, + { "sd", "sudan" }, + { "se", "sweden" }, + { "sg", "singapore" }, + { "sh", "st. helena" }, + { "si", "slovenia" }, + { "sj", "svalbard and jan mayen islands" }, + { "sk", "slovak republic" }, + { "sl", "sierra leone" }, + { "sm", "san marino" }, + { "sn", "senegal" }, + { "so", "somalia" }, + { "sr", "suriname" }, + { "st", "sao tome and principe" }, + { "su", "ussr (former)" }, + { "sv", "el salvador" }, + { "sy", "syria" }, + { "sz", "swaziland" }, + { "tc", "turks and caicos islands" }, + { "td", "chad" }, + { "tf", "french southern territories" }, + { "tg", "togo" }, + { "th", "thailand" }, + { "tj", "tajikistan" }, + { "tk", "tokelau" }, + { "tm", "turkmenistan" }, + { "tn", "tunisia" }, + { "to", "tonga" }, + { "tp", "east timor" }, + { "tr", "turkey" }, + { "tt", "trinidad and tobago" }, + { "tv", "tuvalu" }, + { "tw", "taiwan" }, + { "tz", "tanzania" }, + { "ua", "ukraine" }, + { "ug", "uganda" }, + { "uk", "united kingdom" }, + { "um", "us minor outlying islands" }, + { "us", "united states" }, + { "uy", "uruguay" }, + { "uz", "uzbekistan" }, + { "va", "vatican city state (holy see)" }, + { "vc", "saint vincent and the grenadines" }, + { "ve", "venezuela" }, + { "vg", "virgin islands (british)" }, + { "vi", "virgin islands (u.s.)" }, + { "vn", "viet nam" }, + { "vu", "vanuatu" }, + { "wf", "wallis and futuna islands" }, + { "wk", "wake island" }, + { "ws", "samoa" }, + { "yd", "democratic yemen" }, + { "ye", "yemen" }, + { "yt", "mayotte" }, + { "yu", "yugoslavia" }, + { "za", "south africa" }, + { "zm", "zambia" }, + { "zr", "zaire" }, + { "zw", "zimbabwe" }, + + {"gov", "gov"}, + {"mil", "mil"}, + + { 0, 0 } +}; + +int classdebug; + +static int +incountries(char *s, Country *cp) +{ + for(; cp->code != 0; cp++) + if(cistrcmp(s, cp->code) == 0 + || cistrcmp(s, cp->name) == 0) + return 1; + return 0; +} + +static int +indomains(char *s, char **dp) +{ + for(; *dp != nil; dp++) + if(cistrcmp(s, *dp) == 0) + return 1; + + return 0; +} + +int +classify(char *ip, Ndbtuple *t) +{ + int isgov, iscountry, isbadc, isgoodc; + char dom[256]; + char *df[128]; + Ndbtuple *nt, *x; + int n; + + isgov = iscountry = isbadc = 0; + isgoodc = 1; + + for(nt = t; nt != nil; nt = nt->entry){ + if(strcmp(nt->attr, "country") == 0){ + iscountry = 1; + if(incountries(nt->val, badc)){ + if(classdebug)fprint(2, "isbadc\n"); + isbadc = 1; + isgoodc = 0; + } else if(!incountries(nt->val, goodc)){ + if(classdebug)fprint(2, "!isgoodc\n"); + isgoodc = 0; + } + } + + /* domain names can always hurt, even without forward verification */ + if(strcmp(nt->attr, "dom") == 0){ + strncpy(dom, nt->val, sizeof dom); + dom[sizeof(dom)-1] = 0; + n = getfields(dom, df, nelem(df), 0, "."); + + /* a bad country in a domain name is always believed */ + if(incountries(df[n-1], badc)){ + if(classdebug)fprint(2, "isbadc dom\n"); + isbadc = 1; + isgoodc = 0; + } + + /* a goverment in a domain name is always believed */ + if(n > 1 && indomains(df[n-2], gov)) + isgov = 1; + } + } + if(iscountry == 0){ + /* did the forward lookup work? */ + for(nt = t; nt != nil; nt = nt->entry){ + if(strcmp(nt->attr, "ip") == 0 && strcmp(nt->val, ip) == 0) + break; + } + + /* see if the domain name ends in a country code */ + if(nt != nil && (x = ndbfindattr(t, nt, "dom")) != nil){ + strncpy(dom, x->val, sizeof dom); + dom[sizeof(dom)-1] = 0; + n = getfields(dom, df, nelem(df), 0, "."); + if(incountries(df[n-1], allc)) + iscountry = 1; + } + } + if(iscountry == 0) + return Cunknown; + if(isbadc) + return Cbadc; + if(!isgoodc && isgov) + return Cbadgov; + return Cok; +} diff --git a/sys/src/cmd/ip/httpd/content.c b/sys/src/cmd/ip/httpd/content.c new file mode 100755 index 000000000..3f9d44acf --- /dev/null +++ b/sys/src/cmd/ip/httpd/content.c @@ -0,0 +1,172 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +typedef struct Suffix Suffix; +struct Suffix +{ + Suffix *next; + char *suffix; + char *generic; + char *specific; + char *encoding; +}; + +Suffix *suffixes = nil; + +static Suffix* parsesuffix(char*, Suffix*); +static char* skipwhite(char*); +static HContents suffixclass(char*); +static char* towhite(char*); + +int +updateQid(int fd, Qid *q) +{ + Dir *dir; + Qid dq; + + dir = dirfstat(fd); + if(dir == nil) + sysfatal("can't dirfstat"); + dq = dir->qid; + free(dir); + if(q->path == dq.path && q->vers == dq.vers && q->type == dq.type) + return 0; + *q = dq; + return 1; +} + +void +contentinit(void) +{ + static Biobuf *b = nil; + static Qid qid; + char *file, *s; + Suffix *this; + + file = "/sys/lib/mimetype"; + if(b == nil){ /* first time */ + b = Bopen(file, OREAD); + if(b == nil) + sysfatal("can't read from %s", file); + } + if(updateQid(Bfildes(b), &qid) == 0) + return; + Bseek(b, 0, 0); + while(suffixes!=nil){ + this = suffixes; + suffixes = suffixes->next; + free(this->suffix); + free(this->generic); + free(this->specific); + free(this->encoding); + free(this); + } + + while((s = Brdline(b, '\n')) != nil){ + s[Blinelen(b) - 1] = 0; + suffixes = parsesuffix(s, suffixes); + } +} + +static Suffix* +parsesuffix(char *line, Suffix *suffix) +{ + Suffix *s; + char *p, *fields[5]; + int i, nf; + + p = strchr(line, '#'); + if(p != nil) + *p = '\0'; + nf = tokenize(line, fields, 5); + for(i = 0; i < 4; i++) + if(i >= nf || fields[i][0] == '-') + fields[i] = nil; + + if(fields[2] == nil) + fields[1] = nil; + if(fields[1] == nil && fields[3] == nil) + return suffix; + if(fields[0] == nil) + return suffix; + + s = ezalloc(sizeof *s); + s->next = suffix; + s->suffix = estrdup(fields[0]); + if(fields[1] != nil){ + s->generic = estrdup(fields[1]); + s->specific = estrdup(fields[2]); + } + if(fields[3] != nil) + s->encoding = estrdup(fields[3]); + return s; +} + +/* + * classify by file name extensions + */ +HContents +uriclass(HConnect *hc, char *name) +{ + HContents conts; + Suffix *s; + HContent *type, *enc; + char *buf, *p; + + type = nil; + enc = nil; + if((p = strrchr(name, '/')) != nil) + name = p + 1; + buf = hstrdup(hc, name); + while((p = strrchr(buf, '.')) != nil){ + for(s = suffixes; s; s = s->next){ + if(strcmp(p, s->suffix) == 0){ + if(s->generic != nil && type == nil) + type = hmkcontent(hc, s->generic, s->specific, nil); + if(s->encoding != nil && enc == nil) + enc = hmkcontent(hc, s->encoding, nil, nil); + } + } + *p = 0; + } + conts.type = type; + conts.encoding = enc; + return conts; +} + +/* + * classify by initial contents of file + */ +HContents +dataclass(HConnect *hc, char *buf, int n) +{ + HContents conts; + Rune r; + int c, m; + + for(; n > 0; n -= m){ + c = *buf; + if(c < Runeself){ + if(c < 32 && c != '\n' && c != '\r' && c != '\t' && c != '\v'){ + conts.type = nil; + conts.encoding = nil; + return conts; + } + m = 1; + }else{ + m = chartorune(&r, buf); + if(r == Runeerror){ + conts.type = nil; + conts.encoding = nil; + return conts; + } + } + buf += m; + } + conts.type = hmkcontent(hc, "text", "plain", nil); + conts.encoding = nil; + return conts; +} diff --git a/sys/src/cmd/ip/httpd/emem.c b/sys/src/cmd/ip/httpd/emem.c new file mode 100755 index 000000000..47e1e096b --- /dev/null +++ b/sys/src/cmd/ip/httpd/emem.c @@ -0,0 +1,25 @@ +#include <u.h> +#include <libc.h> +#include "httpd.h" + +void* +ezalloc(ulong n) +{ + void *p; + + p = malloc(n); + if(p == nil) + sysfatal("out of memory"); + memset(p, 0, n); + return p; +} + +char* +estrdup(char *s) +{ + s = strdup(s); + if(s == nil) + sysfatal("out of memory"); + return s; +} + diff --git a/sys/src/cmd/ip/httpd/hints.c b/sys/src/cmd/ip/httpd/hints.c new file mode 100755 index 000000000..27dd51043 --- /dev/null +++ b/sys/src/cmd/ip/httpd/hints.c @@ -0,0 +1,297 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +enum{ URLmax = 65536, HINTmax = 20 }; +#define RECIPLOG2 1.44269504089 + +char **urlname; /* array of url strings 1,...,nurl */ +static int nurl; +static uint urltab[URLmax]; /* hashstr(url) 1,...,nurl */ +static int urlnext[URLmax]; /* index urltab of next url in chain */ +static int urlhash[URLmax]; /* initially 0, meaning empty buckets */ + +typedef struct Hint { + ushort url; + uchar prob; +} Hint; +Hint *hints[URLmax]; +uchar nhint[URLmax]; + +vlong +Bfilelen(void *vb) +{ + Biobuf *b; + vlong n; + + b = vb; + n = Bseek(b, 0L, 2); + Bseek(b, 0L, 0); + return n; +} + +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 int +urllookup(uint url) +{ + /* returns +index into urltab, else -hash */ + int j, hash; + + hash = 1 + url%(URLmax-1); + j = urlhash[hash]; + for(;;){ + if(j==0) + return -hash; + if(url==urltab[j]) + return j; + j = urlnext[j]; + } +} + +int +Bage(Biobuf *b) +{ + Dir *dir; + long mtime; + + dir = dirfstat(Bfildes(b)); + if(dir != nil) + mtime = dir->mtime; + else + mtime = 0; + free(dir); + return time(nil) - mtime; +} + +void +urlinit(void) +{ + static Biobuf *b = nil; + static vlong filelen = 0; + vlong newlen; + char *s, *arena; + int i, j, n; + uint url; + char *file; + + if(filelen < 0) + return; + file = "/sys/log/httpd/url"; + if(b == nil){ + b = Bopen(file, OREAD); /* first time */ + if(b == nil){ + syslog(0, HTTPLOG, "no %s, abandon prefetch hints", file); + filelen = -1; + return; + } + } + newlen = Bfilelen(b); /* side effect: rewinds b */ + if(newlen == filelen || Bage(b)<300) + return; + filelen = newlen; + if(filelen < 0) + return; + if(nurl){ /* free existing tables */ + free(urlname[0]); /* arena */ + memset(urlhash,0,sizeof urlhash); + memset(urlnext,0,sizeof urlnext); + nurl = 0; + } + if(urlname==nil) + urlname = (char**)ezalloc(URLmax*sizeof(*urlname)); + arena = (char*)ezalloc(filelen); /* enough for all the strcpy below */ + i = 1; + while((s=Brdline(b,'\n'))!=0){ + /* read lines of the form: 999 /url/path */ + n = Blinelen(b) - 1; + if(n>2 && s[n]=='\n'){ + s[n] = '\0'; + }else{ + sysfatal("missing fields or newline in url-db"); + } + j = strtoul(s,&s,10); + while(*s==' ') + s++; + if(i++!=j) + sysfatal("url-db synchronization error"); + url = hashstr(s); + j = urllookup(url); + if(j>=0) + sysfatal("duplicate url"); + j = -j; + nurl++; + if(nurl>=URLmax){ + syslog(0, HTTPLOG, "urlinit overflow at %s",s); + free(urlname[0]); /* arena */ + memset(urlhash,0,sizeof urlhash); + memset(urlnext,0,sizeof urlnext); + nurl = 0; + return; + } + urltab[nurl] = url; + urlnext[nurl] = urlhash[j]; + urlhash[j] = nurl; + strcpy(arena,s); + urlname[nurl] = arena; + arena += strlen(s)+1; + } + syslog(0, HTTPLOG, "prefetch-hints url=%d (%.1fMB)", nurl, 1.e-6*(URLmax*sizeof(*urlname)+filelen)); + /* b is held open, because namespace will be chopped */ +} + +void +statsinit(void) +{ + static Biobuf *b = nil; + static vlong filelen = 0; + vlong newlen; + int iq, n, i, nstats = 0; + uchar *s, buf[3+HINTmax*3]; /* iq, n, (url,prob)... */ + Hint *arena, *h; + char *file; + static void *oldarena = nil; + + file = "/sys/log/httpd/pathstat"; + if(b == nil){ + if(filelen == -1) + return; /* if failed first time */ + b = Bopen(file, OREAD); /* first time */ + if(b == nil){ + syslog(0, HTTPLOG, "no %s, abandon prefetch hints", file); + filelen = -1; + return; + } + } + newlen = Bfilelen(b); /* side effect: rewinds b */ + if(newlen == filelen || Bage(b)<300) + return; + filelen = newlen; + if(oldarena){ + free(oldarena); + memset(nhint,0,sizeof nhint); + } + arena = (Hint*)ezalloc((filelen/3)*sizeof(Hint)); + oldarena = arena; + for(;;){ + i = Bread(b,buf,3); + if(i<3) + break; + nstats++; + iq = buf[0]; + iq = (iq<<8) | buf[1]; + n = buf[2]; + h = arena; + arena += n; + hints[iq] = h; + nhint[iq] = n; + if(Bread(b,buf,3*n)!=3*n) + sysfatal("stats read error"); + for(i=0; i<n; i++){ + s = &buf[3*i]; + h[i].url = (s[0]<<8) | s[1]; + h[i].prob = s[2]; + } + } + syslog(0, HTTPLOG, "prefetch-hints stats=%d (%.1fMB)", nstats, 1.e-6*((filelen/3)*sizeof(Hint))); +} + +void +urlcanon(char *url) +{ + /* all the changes here can be implemented by rewriting in-place */ + char *p, *q; + + /* remove extraneous '/' in the middle and at the end */ + p = url+1; /* first char needs no change */ + q = p; + while(q[0]){ + if(q[0]=='/' && q[-1]=='/'){ + q++; + continue; + } + *p++ = *q++; + } + if(q[-1]=='/'){ /* trailing '/' */ + p[-1] = '\0'; + }else{ + p[0] = '\0'; + } + + /* specific to the cm.bell-labs.com web site */ + if(strncmp(url,"/cm/",4)==0){ + if(strchr("cims",url[4]) && strncmp(url+5,"s/who/",6)==0) + /* strip off /cm/cs */ + memmove(url,url+6,strlen(url+6)+1); + else if(strncmp(url+4,"ms/what/wavelet",15)==0) + /* /cm/ms/what */ + memmove(url,url+11,strlen(url+11)+1); + } +} + +void +hintprint(HConnect *hc, Hio *hout, char *uri, int thresh, int havej) +{ + int i, j, pr, prefix, fd, siz, havei, newhint = 0, n; + char *query, *sf, etag[32], *wurl; + Dir *dir; + Hint *h, *haveh; + + query = hstrdup(hc, uri); + urlcanon(query); + j = urllookup(hashstr(query)); + if(j < 0) + return; + query = strrchr(uri,'/'); + if(!query) + return; /* can't happen */ + prefix = query-uri+1; /* = strlen(dirname)+1 */ + h = hints[j]; + for(i=0; i<nhint[j]; i++){ + if(havej > 0 && havej < URLmax){ /* exclude hints client has */ + haveh = hints[havej]; + for(havei=0; havei<nhint[havej]; havei++) + if( haveh[havei].url == h[i].url) + goto continuei; + } + sf = urlname[h[i].url]; + pr = h[i].prob; + if(pr<thresh) + break; + n = strlen(webroot) + strlen(sf) + 1; + wurl = halloc(hc, n); + strcpy(wurl, webroot); + strcat(wurl, sf); + fd = open(wurl, OREAD); + if(fd<0) + continue; + dir = dirfstat(fd); + if(dir == nil){ + close(fd); + continue; + } + close(fd); + snprint(etag, sizeof(etag), "\"%lluxv%lux\"", dir->qid.path, dir->qid.vers); + siz = (int)( log((double)dir->length) * RECIPLOG2 + 0.9999); + free(dir); + if(strncmp(uri,sf,prefix)==0 && strchr(sf+prefix,'/')==0 && sf[prefix]!=0) + sf = sf+prefix; + hprint(hout, "Fresh: %d,%s,%d,%s\r\n", pr, etag, siz, sf); + newhint++; +continuei: ; + } + if(newhint) + hprint(hout, "Fresh: have/%d\r\n", j); +} + diff --git a/sys/src/cmd/ip/httpd/httpd.c b/sys/src/cmd/ip/httpd/httpd.c new file mode 100755 index 000000000..895bd15e2 --- /dev/null +++ b/sys/src/cmd/ip/httpd/httpd.c @@ -0,0 +1,609 @@ +#include <u.h> +#include <libc.h> +#include <auth.h> +#include <mp.h> +#include <libsec.h> +#include "httpd.h" +#include "httpsrv.h" + +typedef struct Strings Strings; + +struct Strings +{ + char *s1; + char *s2; +}; + +char *netdir; +char *HTTPLOG = "httpd/log"; + +static char netdirb[256]; +static char *namespace; + +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 void +dolisten(char *address) +{ + HSPriv *hp; + HConnect *c; + NetConnInfo *nci; + char ndir[NETPATHLEN], dir[NETPATHLEN], *p, *scheme; + int ctl, nctl, data, t, ok, spotchk; + 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; + } + + /* + * 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); + + 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){ + 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; +} diff --git a/sys/src/cmd/ip/httpd/httpsrv.h b/sys/src/cmd/ip/httpd/httpsrv.h new file mode 100755 index 000000000..e279f0f88 --- /dev/null +++ b/sys/src/cmd/ip/httpd/httpsrv.h @@ -0,0 +1,79 @@ +typedef struct HSPriv HSPriv; + +enum +{ + HSTIMEOUT = 15 * 60 * 1000, + + /* rewrite replacement field modifiers */ + Modsilent = '@', /* don't tell the browser about the redirect. */ + Modperm = '=', /* generate permanent redirection */ + Modsubord = '*', /* map page & all subordinates to same URL */ + Modonly = '>', /* match only this page, not subordinates */ + + Redirsilent = 1<<0, + Redirperm = 1<<1, + Redirsubord = 1<<2, + Redironly = 1<<3, +}; + +struct HSPriv +{ + char *remotesys; + char *remoteserv; +}; + +extern int logall[3]; +extern char* HTTPLOG; +extern char* webroot; +extern char* netdir; + +#define STRLEN(s) (sizeof(s)-1) + +/* emem.c */ +char *estrdup(char*); +void* ezalloc(ulong); + +/* sendfd.c */ +int authcheck(HConnect *c); +int checkreq(HConnect *c, HContent *type, HContent *enc, long mtime, char *etag); +int etagmatch(int, HETag*, char*); +HRange *fixrange(HRange *h, long length); +int sendfd(HConnect *c, int fd, Dir *dir, HContent *type, HContent *enc); + +/* content.c */ +void contentinit(void); +HContents dataclass(HConnect *, char*, int); +int updateQid(int, Qid*); +HContents uriclass(HConnect *, char*); + +/* anonymous.c */ +void anonymous(HConnect*); + +/* hint.c */ +void hintprint(HConnect *hc, Hio*, char *, int, int); +void statsinit(void); +void urlcanon(char *url); +void urlinit(void); + +/* init.c */ +HConnect* init(int, char**); + +vlong Bfilelen(void*); + +/* redirect.c */ +void redirectinit(void); +char* redirect(HConnect *hc, char*, uint *); +char* masquerade(char*); +char* authrealm(HConnect *hc, char *path); +char *undecorated(char *repl); + +/* log.c */ +void logit(HConnect*, char*, ...); +#pragma varargck argpos logit 2 +void writelog(HConnect*, char*, ...); +#pragma varargck argpos writelog 2 + +/* authorize.c */ +int authorize(HConnect*, char*); + +char *webroot; diff --git a/sys/src/cmd/ip/httpd/imagemap.c b/sys/src/cmd/ip/httpd/imagemap.c new file mode 100755 index 000000000..9ecf42782 --- /dev/null +++ b/sys/src/cmd/ip/httpd/imagemap.c @@ -0,0 +1,320 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +typedef struct Point Point; +typedef struct OkPoint OkPoint; +typedef struct Strings Strings; + +struct Point +{ + int x; + int y; +}; + +struct OkPoint +{ + Point p; + int ok; +}; + +struct Strings +{ + char *s1; + char *s2; +}; + +static char *me; + +int polytest(int, Point, Point, Point); +Strings getfield(char*); +OkPoint pt(char*); +char* translate(HConnect*, char*, char*); +Point sub(Point, Point); +float dist(Point, Point); + +void +main(int argc, char **argv) +{ + HConnect *c; + Hio *hout; + char *dest; + + me = "imagemap"; + c = init(argc, argv); + hout = &c->hout; + if(hparseheaders(c, HSTIMEOUT) < 0) + exits("failed"); + anonymous(c); + + if(strcmp(c->req.meth, "GET") != 0 && strcmp(c->req.meth, "HEAD") != 0){ + hunallowed(c, "GET, HEAD"); + exits("unallowed"); + } + if(c->head.expectother || c->head.expectcont){ + hfail(c, HExpectFail, nil); + exits("failed"); + } + dest = translate(c, c->req.uri, c->req.search); + + if(dest == nil){ + if(c->req.vermaj){ + hokheaders(c); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + hprint(hout, "<head><title>Nothing Found</title></head><body>\n"); + hprint(hout, "Nothing satisfying your search request could be found.\n</body>\n"); + hflush(hout); + writelog(c, "Reply: 200 imagemap %ld %ld\n", hout->seek, hout->seek); + exits(nil); + } + + if(http11(c) && strcmp(c->req.meth, "POST") == 0) + hredirected(c, "303 See Other", dest); + else + hredirected(c, "302 Found", dest); + exits(nil); +} + +char* +translate(HConnect *c, char *uri, char *search) +{ + Biobuf *b; + Strings ss; + OkPoint okp; + Point p, cen, q, start; + float close, d; + char *line, *to, *def, *s, *dst; + int n, inside, r, ncsa; + + if(search == nil){ + hfail(c, HNoData, me); + exits("failed"); + } + okp = pt(search); + if(!okp.ok){ + hfail(c, HBadSearch, me); + exits("failed"); + } + p = okp.p; + + b = Bopen(uri, OREAD); + if(b == nil){ + hfail(c, HNotFound, uri); + exits("failed"); + } + + to = nil; + def = nil; + dst = nil; + close = 0.; + ncsa = 1; + while(line = Brdline(b, '\n')){ + line[Blinelen(b)-1] = 0; + + ss = getfield(line); + s = ss.s1; + line = ss.s2; + if(ncsa){ + ss = getfield(line); + dst = ss.s1; + line = ss.s2; + } + if(strcmp(s, "#cern") == 0){ + ncsa = 0; + continue; + } + if(strcmp(s, "rect") == 0){ + ss = getfield(line); + s = ss.s1; + line = ss.s2; + okp = pt(s); + q = okp.p; + if(!okp.ok || q.x > p.x || q.y > p.y) + continue; + ss = getfield(line); + s = ss.s1; + line = ss.s2; + okp = pt(s); + q = okp.p; + if(!okp.ok || q.x < p.x || q.y < p.y) + continue; + if(!ncsa){ + ss = getfield(line); + dst = ss.s1; + } + return dst; + }else if(strcmp(s, "circle") == 0){ + ss = getfield(line); + s = ss.s1; + line = ss.s2; + okp = pt(s); + cen = okp.p; + if(!okp.ok) + continue; + ss = getfield(line); + s = ss.s1; + line = ss.s2; + if(ncsa){ + okp = pt(s); + if(!okp.ok) + continue; + if(dist(okp.p, cen) >= dist(p, cen)) + return dst; + }else{ + r = strtol(s, nil, 10); + ss = getfield(line); + dst = ss.s1; + d = (float)r * r; + if(d >= dist(p, cen)) + return dst; + } + }else if(strcmp(s, "poly") == 0){ + ss = getfield(line); + s = ss.s1; + line = ss.s2; + okp = pt(s); + start = okp.p; + if(!okp.ok) + continue; + inside = 0; + cen = start; + for(n = 1; ; n++){ + ss = getfield(line); + s = ss.s1; + line = ss.s2; + okp = pt(s); + q = okp.p; + if(!okp.ok) + break; + inside = polytest(inside, p, cen, q); + cen = q; + } + inside = polytest(inside, p, cen, start); + if(!ncsa) + dst = s; + if(n >= 3 && inside) + return dst; + }else if(strcmp(s, "point") == 0){ + ss = getfield(line); + s = ss.s1; + line = ss.s2; + okp = pt(s); + q = okp.p; + if(!okp.ok) + continue; + d = dist(p, q); + if(!ncsa){ + ss = getfield(line); + dst = ss.s1; + } + if(d == 0.) + return dst; + if(close == 0. || d < close){ + close = d; + to = dst; + } + }else if(strcmp(s, "default") == 0){ + if(!ncsa){ + ss = getfield(line); + dst = ss.s1; + } + def = dst; + } + } + if(to == nil) + to = def; + return to; +} + +int +polytest(int inside, Point p, Point b, Point a) +{ + Point pa, ba; + + if(b.y>a.y){ + pa=sub(p, a); + ba=sub(b, a); + }else{ + pa=sub(p, b); + ba=sub(a, b); + } + if(0<=pa.y && pa.y<ba.y && pa.y*ba.x<=pa.x*ba.y) + inside = !inside; + return inside; +} + +Point +sub(Point p, Point q) +{ + p.x -= q.x; + p.y -= q.y; + return p; +} + +float +dist(Point p, Point q) +{ + p.x -= q.x; + p.y -= q.y; + return (float)p.x * p.x + (float)p.y * p.y; +} + +OkPoint +pt(char *s) +{ + OkPoint okp; + Point p; + char *t, *e; + + if(*s == '(') + s++; + t = strchr(s, ')'); + if(t != nil) + *t = 0; + p.x = 0; + p.y = 0; + t = strchr(s, ','); + if(t == nil){ + okp.p = p; + okp.ok = 0; + return okp; + } + e = nil; + p.x = strtol(s, &e, 10); + if(e != t){ + okp.p = p; + okp.ok = 0; + return okp; + } + p.y = strtol(t+1, &e, 10); + if(e == nil || *e != 0){ + okp.p = p; + okp.ok = 0; + return okp; + } + okp.p = p; + okp.ok = 1; + return okp; +} + +Strings +getfield(char *s) +{ + Strings ss; + char *f; + + while(*s == '\t' || *s == ' ') + s++; + f = s; + while(*s && *s != '\t' && *s != ' ') + s++; + if(*s) + *s++ = 0; + ss.s1 = f; + ss.s2 = s; + return ss; +} diff --git a/sys/src/cmd/ip/httpd/init.c b/sys/src/cmd/ip/httpd/init.c new file mode 100755 index 000000000..8d3eb0624 --- /dev/null +++ b/sys/src/cmd/ip/httpd/init.c @@ -0,0 +1,113 @@ +#include <u.h> +#include <libc.h> +#include "httpd.h" +#include "httpsrv.h" + +void +usage(void) +{ + fprint(2, "usage: %s [-b inbuf] [-d domain] [-p localport]" + " [-r remoteip] [-s uri-scheme] [-w webroot]" + " [-L logfd0 logfd1] [-N netdir] [-R reqline]" + " method version uri [search]\n", argv0); + exits("usage"); +} + +char *netdir; +char *webroot; +char *HTTPLOG = "httpd/log"; + +static HConnect connect; +static HSPriv priv; + +HConnect* +init(int argc, char **argv) +{ + char *vs; + + hinit(&connect.hin, 0, Hread); + hinit(&connect.hout, 1, Hwrite); + hmydomain = nil; + connect.replog = writelog; + connect.scheme = "http"; + connect.port = "80"; + connect.private = &priv; + priv.remotesys = nil; + priv.remoteserv = nil; + fmtinstall('D', hdatefmt); + fmtinstall('H', httpfmt); + fmtinstall('U', hurlfmt); + netdir = "/net"; + ARGBEGIN{ + case 'b': + hload(&connect.hin, EARGF(usage())); + break; + case 'd': + hmydomain = EARGF(usage()); + break; + case 'p': + connect.port = EARGF(usage()); + break; + case 'r': + priv.remotesys = EARGF(usage()); + break; + case 's': + connect.scheme = EARGF(usage()); + break; + case 'w': + webroot = EARGF(usage()); + break; + case 'L': + logall[0] = strtol(EARGF(usage()), nil, 10); + logall[1] = strtol(EARGF(usage()), nil, 10); + break; + case 'N': + netdir = EARGF(usage()); + break; + case 'R': + snprint((char*)connect.header, sizeof(connect.header), "%s", + EARGF(usage())); + break; + default: + usage(); + }ARGEND + + if(priv.remotesys == nil) + priv.remotesys = "unknown"; + if(priv.remoteserv == nil) + priv.remoteserv = "unknown"; + if(hmydomain == nil) + hmydomain = "unknown"; + if(webroot == nil) + webroot = "/usr/web"; + + /* + * open all files we might need before castrating namespace + */ + time(nil); + syslog(0, HTTPLOG, nil); + + if(argc != 4 && argc != 3) + usage(); + + connect.req.meth = argv[0]; + + vs = argv[1]; + connect.req.vermaj = 0; + connect.req.vermin = 9; + if(strncmp(vs, "HTTP/", 5) == 0){ + vs += 5; + connect.req.vermaj = strtoul(vs, &vs, 10); + if(*vs == '.') + vs++; + connect.req.vermin = strtoul(vs, &vs, 10); + } + + connect.req.uri = argv[2]; + connect.req.search = argv[3]; + connect.head.closeit = 1; + connect.hpos = (uchar*)strchr((char*)connect.header, '\0'); + connect.hstop = connect.hpos; + connect.reqtime = time(nil); /* not quite right, but close enough */ + return &connect; +} diff --git a/sys/src/cmd/ip/httpd/log.c b/sys/src/cmd/ip/httpd/log.c new file mode 100755 index 000000000..ad97238e9 --- /dev/null +++ b/sys/src/cmd/ip/httpd/log.c @@ -0,0 +1,104 @@ +#include <u.h> +#include <libc.h> +#include "httpd.h" +#include "httpsrv.h" + +int logall[3]; /* logall[2] is in "Common Log Format" */ + +static char * +monname[12] = +{ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" +}; + +void +logit(HConnect *c, char *fmt, ...) +{ + char buf[4096]; + va_list arg; + HSPriv *p; + + va_start(arg, fmt); + vseprint(buf, buf+sizeof(buf), fmt, arg); + va_end(arg); + p = nil; + if(c != nil) + p = c->private; + if(p != nil && p->remotesys != nil) + syslog(0, HTTPLOG, "%s %s", p->remotesys, buf); + else + syslog(0, HTTPLOG, "%s", buf); +} + +void +writelog(HConnect *c, char *fmt, ...) +{ + HSPriv *p; + char buf[HBufSize+500], *bufp, *bufe; + char statuscode[4]; + vlong objectsize; + ulong now, today; + int logfd; + va_list arg; + Tm *tm; + + if(c == nil) + return; + p = c->private; + bufe = buf + sizeof(buf); + now = time(nil); + tm = gmtime(now); + today = now / (24*60*60); + + /* verbose logfile, for research on web traffic */ + logfd = logall[today & 1]; + if(logfd > 0){ + if(c->hstop == c->header || c->hstop[-1] != '\n') + *c->hstop = '\n'; + *c->hstop = '\0'; + bufp = seprint(buf, bufe, "==========\n"); + bufp = seprint(bufp, bufe, "LogTime: %D\n", now); + bufp = seprint(bufp, bufe, "ConnTime: %D\n", c->reqtime); + bufp = seprint(bufp, bufe, "RemoteIP: %s\n", p->remotesys); + bufp = seprint(bufp, bufe, "Port: %s\n", p->remoteserv); + va_start(arg, fmt); + bufp = vseprint(bufp, bufe, fmt, arg); + va_end(arg); + if(c->req.uri != nil && c->req.uri[0] != 0) + bufp = seprint(bufp, bufe, "FinalURI: %s\n", c->req.uri); + bufp = seprint(bufp, bufe, "----------\n%s\n", (char*)c->header); + write(logfd, buf, bufp-buf); /* append-only file */ + } + + /* another log, with less information but formatted for common analysis tools */ + if(logall[2] > 0 && strncmp(fmt, "Reply: ", 7) == 0){ + objectsize = 0; + strecpy(statuscode, statuscode+4, fmt+7); + if( fmt[7] == '%'){ + va_start(arg, fmt); + vseprint(statuscode, statuscode+4, fmt+7, arg); + va_end(arg); + }else if( + strcmp(fmt+7, "200 file %lld %lld\n") == 0 || + strcmp(fmt+7, "206 partial content %lld %lld\n") == 0 || + strcmp(fmt+7, "206 partial content, early termination %lld %lld\n") == 0){ + va_start(arg, fmt); + objectsize = va_arg(arg, vlong); /* length in sendfd.c */ + USED(objectsize); + objectsize = va_arg(arg, vlong); /* wrote in sendfd.c */ + va_end(arg); + } + bufp = seprint(buf, bufe, "%s - -", p->remotesys); + bufp = seprint(bufp, bufe, " [%.2d/%s/%d:%.2d:%.2d:%.2d +0000]", tm->mday, monname[tm->mon], tm->year+1900, tm->hour, tm->min, tm->sec); + if(c->req.uri == nil || c->req.uri[0] == 0){ + bufp = seprint(bufp, bufe, " \"%.*s\"", + (int)utfnlen((char*)c->header, strcspn((char*)c->header, "\r\n")), + (char*)c->header); + }else{ + /* use more canonical form of URI, if available */ + bufp = seprint(bufp, bufe, " \"%s %s HTTP/%d.%d\"", c->req.meth, c->req.uri, c->req.vermaj, c->req.vermin); + } + bufp = seprint(bufp, bufe, " %s %lld\n", statuscode, objectsize); + write(logall[2], buf, bufp-buf); /* append-only file */ + } +} diff --git a/sys/src/cmd/ip/httpd/man2html.c b/sys/src/cmd/ip/httpd/man2html.c new file mode 100755 index 000000000..7971bf3bf --- /dev/null +++ b/sys/src/cmd/ip/httpd/man2html.c @@ -0,0 +1,449 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +static Hio *hout; +static Hio houtb; +static HConnect *connect; + +void doconvert(char*, int); + +void +error(char *title, char *fmt, ...) +{ + va_list arg; + char buf[1024], *out; + + va_start(arg, fmt); + out = vseprint(buf, buf+sizeof(buf), fmt, arg); + va_end(arg); + *out = 0; + + hprint(hout, "%s 404 %s\n", hversion, title); + hprint(hout, "Date: %D\n", time(nil)); + hprint(hout, "Server: Plan9\n"); + hprint(hout, "Content-type: text/html\n"); + hprint(hout, "\n"); + hprint(hout, "<head><title>%s</title></head>\n", title); + hprint(hout, "<body><h1>%s</h1></body>\n", title); + hprint(hout, "%s\n", buf); + hflush(hout); + writelog(connect, "Reply: 404\nReason: %s\n", title); + exits(nil); +} + +typedef struct Hit Hit; +struct Hit +{ + Hit *next; + char *file; +}; + +void +lookup(char *object, int section, Hit **list) +{ + int fd; + char *p, *f; + Biobuf b; + char file[256]; + Hit *h; + + while(*list != nil) + list = &(*list)->next; + + snprint(file, sizeof(file), "/sys/man/%d/INDEX", section); + fd = open(file, OREAD); + if(fd > 0){ + Binit(&b, fd, OREAD); + for(;;){ + p = Brdline(&b, '\n'); + if(p == nil) + break; + p[Blinelen(&b)-1] = 0; + f = strchr(p, ' '); + if(f == nil) + continue; + *f++ = 0; + if(strcmp(p, object) == 0){ + h = ezalloc(sizeof *h); + *list = h; + h->next = nil; + snprint(file, sizeof(file), "/%d/%s", section, f); + h->file = estrdup(file); + close(fd); + return; + } + } + close(fd); + } + snprint(file, sizeof(file), "/sys/man/%d/%s", section, object); + if(access(file, 0) == 0){ + h = ezalloc(sizeof *h); + *list = h; + h->next = nil; + h->file = estrdup(file+8); + } +} + +void +manindex(int sect, int vermaj) +{ + int i; + + if(vermaj){ + hokheaders(connect); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + + hprint(hout, "<head><title>plan 9 section index"); + if(sect) + hprint(hout, "(%d)\n", sect); + hprint(hout, "</title></head><body>\n"); + hprint(hout, "<H6>Section Index"); + if(sect) + hprint(hout, "(%d)\n", sect); + hprint(hout, "</H6>\n"); + + if(sect) + hprint(hout, "<p><a href=\"/plan9/man%d.html\">/plan9/man%d.html</a>\n", + sect, sect); + else for(i = 1; i < 10; i++) + hprint(hout, "<p><a href=\"/plan9/man%d.html\">/plan9/man%d.html</a>\n", + i, i); + hprint(hout, "</body>\n"); +} + +void +man(char *o, int sect, int vermaj) +{ + int i; + Hit *list; + + list = nil; + + if(*o == 0){ + manindex(sect, vermaj); + return; + } + + if(sect > 0 && sect < 10) + lookup(o, sect, &list); + else + for(i = 1; i < 9; i++) + lookup(o, i, &list); + + if(list != nil && list->next == nil){ + doconvert(list->file, vermaj); + return; + } + + if(vermaj){ + hokheaders(connect); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + + hprint(hout, "<head><title>plan 9 man %H", o); + if(sect) + hprint(hout, "(%d)\n", sect); + hprint(hout, "</title></head><body>\n"); + hprint(hout, "<H6>Search for %H", o); + if(sect) + hprint(hout, "(%d)\n", sect); + hprint(hout, "</H6>\n"); + + for(; list; list = list->next) + hprint(hout, "<p><a href=\"/magic/man2html%U\">/magic/man2html%H</a>\n", + list->file, list->file); + hprint(hout, "</body>\n"); +} + +void +strlwr(char *p) +{ + for(; *p; p++) + if('A' <= *p && *p <= 'Z') + *p += 'a'-'A'; +} + +void +redirectto(char *uri) +{ + if(connect){ + hmoved(connect, uri); + exits(0); + }else + hprint(hout, "Your selection moved to <a href=\"%U\"> here</a>.<p></body>\r\n", uri); +} + +void +searchfor(char *search) +{ + int i, j, n, fd; + char *p, *sp; + Biobufhdr *b; + char *arg[32]; + + hprint(hout, "<head><title>plan 9 search for %H</title></head>\n", search); + hprint(hout, "<body>\n"); + + hprint(hout, "<p>This is a keyword search through Plan 9 man pages.\n"); + hprint(hout, "The search is case insensitive; blanks denote \"boolean and\".\n"); + hprint(hout, "<FORM METHOD=\"GET\" ACTION=\"/magic/man2html\">\n"); + hprint(hout, "<INPUT NAME=\"pat\" TYPE=\"text\" SIZE=\"60\">\n"); + hprint(hout, "<INPUT TYPE=\"submit\" VALUE=\"Submit\">\n"); + hprint(hout, "</FORM>\n"); + + hprint(hout, "<hr><H6>Search for %H</H6>\n", search); + n = getfields(search, arg, 32, 1, "+"); + for(i = 0; i < n; i++){ + for(j = i+1; j < n; j++){ + if(strcmp(arg[i], arg[j]) > 0){ + sp = arg[j]; + arg[j] = arg[i]; + arg[i] = sp; + } + } + sp = malloc(strlen(arg[i]) + 2); + if(sp != nil){ + strcpy(sp+1, arg[i]); + sp[0] = ' '; + arg[i] = sp; + } + } + + /* + * search index till line starts alphabetically < first token + */ + fd = open("/sys/man/searchindex", OREAD); + if(fd < 0){ + hprint(hout, "<body>error: No Plan 9 search index\n"); + hprint(hout, "</body>"); + return; + } + p = malloc(32*1024); + if(p == nil){ + close(fd); + return; + } + b = ezalloc(sizeof *b); + Binits(b, fd, OREAD, (uchar*)p, 32*1024); + for(;;){ + p = Brdline(b, '\n'); + if(p == nil) + break; + p[Blinelen(b)-1] = 0; + for(i = 0; i < n; i++){ + sp = strstr(p, arg[i]); + if(sp == nil) + break; + p = sp; + } + if(i < n) + continue; + sp = strrchr(p, '\t'); + if(sp == nil) + continue; + sp++; + hprint(hout, "<p><a href=\"/magic/man2html/%U\">/magic/man2html/%H</a>\n", + sp, sp); + } + hprint(hout, "</body>"); + + Bterm(b); + free(b); + free(p); + close(fd); +} + +/* + * find man pages mentioning the search string + */ +void +dosearch(int vermaj, char *search) +{ + int sect; + char *p; + + if(strncmp(search, "man=", 4) == 0){ + sect = 0; + search = hurlunesc(connect, search+4); + p = strchr(search, '&'); + if(p != nil){ + *p++ = 0; + if(strncmp(p, "sect=", 5) == 0) + sect = atoi(p+5); + } + man(search, sect, vermaj); + return; + } + + if(vermaj){ + hokheaders(connect); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + + if(strncmp(search, "pat=", 4) == 0){ + search = hurlunesc(connect, search+4); + search = hlower(search); + searchfor(search); + return; + } + + hprint(hout, "<head><title>illegal search</title></head>\n"); + hprint(hout, "<body><p>Illegally formatted Plan 9 man page search</p>\n"); + search = hurlunesc(connect, search); + hprint(hout, "<body><p>%H</p>\n", search); + hprint(hout, "</body>"); +} + +/* + * convert a man page to html and output + */ +void +doconvert(char *uri, int vermaj) +{ + char *p; + char file[256]; + char title[256]; + char err[ERRMAX]; + int pfd[2]; + Dir *d; + Waitmsg *w; + int x; + + if(strstr(uri, "..")) + error("bad URI", "man page URI cannot contain .."); + p = strstr(uri, "/intro"); + + if(p == nil){ + while(*uri == '/') + uri++; + /* redirect section requests */ + snprint(file, sizeof(file), "/sys/man/%s", uri); + d = dirstat(file); + if(d == nil){ + strlwr(file); + if(dirstat(file) != nil){ + snprint(file, sizeof(file), "/magic/man2html/%s", uri); + strlwr(file); + redirectto(file); + } + error(uri, "man page not found"); + } + x = d->qid.type; + free(d); + if(x & QTDIR){ + if(*uri == 0 || strcmp(uri, "/") == 0) + redirectto("/sys/man/index.html"); + else { + snprint(file, sizeof(file), "/sys/man/%s/INDEX.html", + uri+1); + redirectto(file); + } + return; + } + } else { + /* rewrite the name intro */ + *p = 0; + snprint(file, sizeof(file), "/sys/man/%s/0intro", uri); + d = dirstat(file); + free(d); + if(d == nil) + error(uri, "man page not found"); + } + + if(vermaj){ + hokheaders(connect); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + hflush(hout); + + if(pipe(pfd) < 0) + error("out of resources", "pipe failed"); + + /* troff -manhtml <file> | troff2html -t '' */ + switch(fork()){ + case -1: + error("out of resources", "fork failed"); + case 0: + snprint(title, sizeof(title), "Plan 9 %s", file); + close(0); + dup(pfd[0], 0); + close(pfd[0]); + close(pfd[1]); + execl("/bin/troff2html", "troff2html", "-t", title, nil); + errstr(err, sizeof err); + exits(err); + } + switch(fork()){ + case -1: + error("out of resources", "fork failed"); + case 0: + snprint(title, sizeof(title), "Plan 9 %s", file); + close(0); + close(1); + dup(pfd[1], 1); + close(pfd[0]); + close(pfd[1]); + execl("/bin/troff", "troff", "-manhtml", file, nil); + errstr(err, sizeof err); + exits(err); + } + close(pfd[0]); + close(pfd[1]); + + /* wait for completion */ + for(;;){ + w = wait(); + if(w == nil) + break; + if(w->msg[0] != 0) + print("whoops %s\n", w->msg); + free(w); + } +} + +void +main(int argc, char **argv) +{ + fmtinstall('H', httpfmt); + fmtinstall('U', hurlfmt); + + if(argc == 2){ + hinit(&houtb, 1, Hwrite); + hout = &houtb; + doconvert(argv[1], 0); + exits(nil); + } + close(2); + + connect = init(argc, argv); + hout = &connect->hout; + if(hparseheaders(connect, HSTIMEOUT) < 0) + exits("failed"); + + if(strcmp(connect->req.meth, "GET") != 0 && strcmp(connect->req.meth, "HEAD") != 0){ + hunallowed(connect, "GET, HEAD"); + exits("not allowed"); + } + if(connect->head.expectother || connect->head.expectcont){ + hfail(connect, HExpectFail, nil); + exits("failed"); + } + + bind("/usr/web/sys/man", "/sys/man", MREPL); + + if(connect->req.search != nil) + dosearch(connect->req.vermaj, connect->req.search); + else + doconvert(connect->req.uri, connect->req.vermaj); + hflush(hout); + writelog(connect, "200 man2html %ld %ld\n", hout->seek, hout->seek); + exits(nil); +} diff --git a/sys/src/cmd/ip/httpd/mkfile b/sys/src/cmd/ip/httpd/mkfile new file mode 100755 index 000000000..fe8b67d92 --- /dev/null +++ b/sys/src/cmd/ip/httpd/mkfile @@ -0,0 +1,79 @@ +</$objtype/mkfile + +HFILES=\ + /sys/include/httpd.h\ + httpsrv.h\ + +TARG=\ + httpd\ + imagemap\ + man2html\ + save\ + netlib_find\ + netlib_history\ + webls\ + wikipost\ + +XTARG=\ + httpd\ + imagemap\ + netlib_find\ + netlib_history\ + man2html\ + save\ + wikipost\ + +LIB=libhttps.a$O + +LIBS=libhttps.a$O +LIBSOFILES=\ + anonymous.$O\ + content.$O\ + emem.$O\ + hints.$O\ + init.$O\ + log.$O\ + redirect.$O\ + sendfd.$O\ + authorize.$O\ + +BIN=/$objtype/bin/ip/httpd + +UPDATE=\ + $HFILES\ + ${LIBSOFILES:%.$O=%.c}\ + ${XTARG:%=%.c}\ + +</sys/src/cmd/mkmany + +trial: $O.netlib_history + # should first mount -b /srv/histnetlib /usr/web/historic + echo ' + ' | $O.netlib_history GET HTTP/1.0 xxx 'file=fp%2Fdtoa.c.gz' + +trial2: $O.netlib_find + echo "\n" | $O.netlib_find GET HTTP/1.0 xxx 'db=1&pat=Hearing' > /tmp/search + sed 17q /tmp/search + +$LIBS: $LIBSOFILES + ar vu $LIBS $newprereq + rm $newprereq + # rm $newmember - cannot do this because of mk race + + +re:N: v.re + v.re redirect.urls + +none:VQ: + echo usage: mk all, install, installall, '$O'.cmd, cmd.install, or cmd.installall + echo usage: mk safeinstall, safeinstallall, cmd.safeinstallall, or cmd.safeinstallall + +$O.9down: 9down.$O whois.$O classify.$O $LIB + $LD -o $target $prereq + +$O.test9down: 9down4e.$O whois.$O classify.$O $LIB + $LD -o $target $prereq + +$O.testclassify: testclassify.$O whois.$O classify.$O $LIB + $LD -o $target $prereq + diff --git a/sys/src/cmd/ip/httpd/netlib_find.c b/sys/src/cmd/ip/httpd/netlib_find.c new file mode 100755 index 000000000..d594e3f7f --- /dev/null +++ b/sys/src/cmd/ip/httpd/netlib_find.c @@ -0,0 +1,278 @@ +/* invoked from /netlib/pub/search.html */ + +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +void bib_fmt(char*,char*); +void index_fmt(char*,char*); +void no_fmt(char*,char*); +int send(HConnect*); + +Hio *hout; + +/********** table of databases ************/ + +typedef struct DB DB; +struct DB +{ + int SELECT; /* value from search.html */ + char *log; /* abbreviation for logfile */ + int maxhit; /* maximum number of hits to return */ + char *file; /* searchfs database */ + void (*fmt)(char*,char*); /* convert one record to HTML */ + char *postlude; /* trailer text */ +}; + +DB db[] = +{ + {0, "netlib", 250, "/srv/netlib_DEFAULT", index_fmt, + "<HR><A HREF=\"/netlib/master\">browse netlib</A></BODY>\r\n"}, + {1, "BibNet", 250, "/srv/netlib_bibnet", bib_fmt, + "<HR><A HREF=\"/netlib/bibnet\">browse BibNet</A></BODY>\r\n"}, + {2, "compgeom", 250, "/srv/netlib_compgeom", no_fmt, "</BODY>\r\n"}, + {3, "approx", 250, "/srv/netlib_approximation", no_fmt, + "<HR><A HREF=\"/netlib/a/catalog.html.gz\">hierarchical catalog</A></BODY>\r\n"}, + {4, "siam", 50, "/srv/netlib_siam-Secret", no_fmt, "</BODY>\r\n"}, + {-1,"",0,"",no_fmt,""} +}; + + + +/********** reformat database record as HTML ************/ + +void /* tr '\015' '\012' ("uncombline") */ +no_fmt(char*s,char*e) +{ + /* s = start, e = (one past) end of database record */ + char *p; + for(p = s; p<e; p++) + if(*p=='\r'){ + hwrite(hout, s,p-s); + hprint(hout, "\n"); + s = p+1; + } +} + +int /* should the filename have .gz appended? */ +suffix(char*filename) +{ + int n; + char *z; + + if(!filename || *filename==0) + return(0); + n = strlen(filename); + if(strncmp(".html",filename+n-5,5)==0) + return(0); + z = malloc(n+50); + if(z == nil) + return(0); + strcpy(z,"/netlib/pub/"); + strcat(z,filename); + strcat(z,".gz"); + if(access(z,4)==0){ + free(z); + return(1); + } + free(z); + return(0); +} + +void /* add HREF to "file:" lines */ +index_fmt(char*s,char*e) +{ + char *p, *filename; + if(strncmp(s,"file",4)==0 && (s[4]==' '||s[4]=='\t')){ + for(filename = s+4; strchr(" \t",*filename); filename++){} + for(s = filename; *s && strchr("\r\n",*s)==nil; s++){} + *s++ = '\0'; + if(*s=='\n') s++; + hprint(hout, "file: <A HREF=\"/netlib/%s",filename); + if(suffix(filename)) + hprint(hout, ".gz"); + hprint(hout, "\">%s</A>\r\n",filename); + for(p = s; p<e; p++) + if(*p=='\r'){ + hwrite(hout, s,p-s); + hprint(hout, "\n"); + s = p+1; + } + }else if(strncmp(s,"lib",3)==0 && (s[3]==' '||s[3]=='\t')){ + for(filename = s+3; strchr(" \t",*filename); filename++){} + for(s = filename; *s && strchr("\r\n",*s)==nil; s++){} + *s++ = '\0'; + if(*s=='\n') s++; + hprint(hout, "lib: <A HREF=\"/netlib/%s",filename); + hprint(hout, "\">%s</A>\r\n",filename); + for(p = s; p<e; p++) + if(*p=='\r'){ + hwrite(hout, s,p-s); + hprint(hout, "\n"); + s = p+1; + } + }else{ + no_fmt(s,e); + } +} + +void /* add HREF to "URL" lines */ +bib_fmt(char*s,char*e) +{ + char *p, *filename; + for(p = s; p<e; p++) + if(*p=='\r'){ + hwrite(hout, s,p-s); + hprint(hout, "\n"); + s = p+1; + if(strncmp(s," URL =",6)==0 && + (filename = strchr(s+6,'"'))!=nil){ + filename++; + for(s = filename; *s && strchr("\"\r\n",*s)==nil; s++){} + *s++ = '\0'; + p = s; + hprint(hout, " URL =<A HREF=\"%s\">%s</A>", + filename,filename); + } + } +} + + +/********** main() calls httpheadget() calls send() ************/ + +void +main(int argc, char **argv) +{ + HConnect *c; + + c = init(argc, argv); + hout = &c->hout; + if(hparseheaders(c, HSTIMEOUT) >= 0) + send(c); + exits(nil); +} + +Biobuf Blist; + +Biobuf* +init800fs(char*name,char*pat) +{ + int fd800fs, n; + char*search; + + fd800fs = open(name, ORDWR); + if(fd800fs < 0) + exits("can't connect to 800fs server"); + if(mount(fd800fs, -1, "/mnt", MREPL, "") < 0) + exits("can't mount /mnt"); + fd800fs = open("/mnt/search", ORDWR); + n = strlen("search=")+strlen(pat)+1; + search = ezalloc(n); + strcpy(search,"search="); + strcat(search,pat); + write(fd800fs,search,n); + free(search); + Binit(&Blist, fd800fs,OREAD); + return(&Blist); +} + + +static char * +hq(char *text) +{ + int textlen = strlen(text), escapedlen = textlen; + char *escaped, *s, *w; + + for(s = text; *s; s++) + if(*s=='<' || *s=='>' || *s=='&') + escapedlen += 4; + escaped = ezalloc(escapedlen+1); + for(s = text, w = escaped; *s; s++){ + if(*s == '<'){ + strcpy(w, "<"); + w += 4; + }else if(*s == '>'){ + strcpy(w, ">"); + w += 4; + }else if(*s == '&'){ + strcpy(w, "&"); + w += 5; + }else{ + *w++ = *s; + } + } + return escaped; +} + +int +send(HConnect *c) +{ + Biobuf*blist; + int m, n, dbi, nmatch; + char *pat, *s, *e; + HSPairs *q; + + 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, nil); + if(c->req.search == nil || !*c->req.search) + return hfail(c, HNoData, "netlib_find"); + s = c->req.search; + while((s = strchr(s, '+')) != nil) + *s++ = ' '; + dbi = -1; + pat = nil; + for(q = hparsequery(c, hstrdup(c, c->req.search)); q; q = q->next){ + if(strcmp(q->s, "db") == 0){ + m = atoi(q->t); + for(dbi = 0; m!=db[dbi].SELECT; dbi++) + if(db[dbi].SELECT<0) + exits("unrecognized db"); + }else if(strcmp(q->s, "pat") == 0){ + pat = q->t; + } + } + if(dbi < 0) + exits("missing db field in query"); + if(pat == nil) + exits("missing pat field in query"); + logit(c, "netlib_find %s %s", db[dbi].log,pat); + + blist = init800fs(db[dbi].file,pat); + + if(c->req.vermaj){ + hokheaders(c); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + if(strcmp(c->req.meth, "HEAD") == 0){ + writelog(c, "Reply: 200 netlib_find 0\n"); + hflush(hout); + exits(nil); + } + + hprint(hout, "<HEAD><TITLE>%s/%s</TITLE></HEAD>\r\n<BODY>\r\n", + db[dbi].log,hq(pat)); + nmatch = 0; + + while(s = Brdline(blist, '\n')){ /* get next database record */ + n = Blinelen(blist); + e = s+n; + hprint(hout, "<PRE>"); + (*db[dbi].fmt)(s,e); + hprint(hout, "</PRE>\r\n"); + if(nmatch++>=db[dbi].maxhit){ + hprint(hout, "<H4>reached limit at %d hits</H4>\n\r",nmatch); + break; + } + } + if(nmatch==0) + hprint(hout, "<H4>Nothing Found.</H4>\r\n"); + hprint(hout, db[dbi].postlude); + hflush(hout); + writelog(c, "Reply: 200 netlib_find %ld %ld\n", hout->seek, hout->seek); + return 1; +} diff --git a/sys/src/cmd/ip/httpd/netlib_history.c b/sys/src/cmd/ip/httpd/netlib_history.c new file mode 100755 index 000000000..d24806225 --- /dev/null +++ b/sys/src/cmd/ip/httpd/netlib_history.c @@ -0,0 +1,218 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +Hio *HO; +int diffb; + +enum{ DAY = 24*60*60 }; + +void +lastbefore(ulong t, char *f, char *b) +{ + Tm *tm; + Dir *dir; + int try; + ulong t0, mtime; + + t0 = t; + for(try=0; try<10; try++) { + tm = localtime(t); + t -= DAY; + sprint(b,"%.4d/%.2d%.2d/netlib/pub/%s",tm->year+1900,tm->mon+1,tm->mday,f); + dir = dirstat(b); + if(dir == nil) + continue; + mtime = dir->mtime; + free(dir); + if(mtime > t0) + continue; + return; + } + strcpy(b, "filenotfound"); +} + +// create explicit file for diff, which otherwise would create a +// mode 0600 file that it couldn't read (because running as none) +void +gunzip(char *f, char *tmp) +{ + int fd = open(tmp, OWRITE); + + if(fd < 0) // can't happen + return; + switch(fork()){ + case 0: + dup(fd, 1); + close(fd); + close(0); + execl("/bin/gunzip", "gunzip", "-c", f, nil); + hprint(HO, "can't exec gunzip: %r\n"); + break; + case -1: + hprint(HO, "fork failed: %r\n"); + default: + while(waitpid() != -1) + ; + break; + } + close(fd); +} + +void +netlibhistory(char *file) +{ + char buf[500], pair[2][500], tmpf[2][30], *f; + int toggle = 0, started = 0, limit; + Dir *dir; + ulong otime, dt; + int i, fd, tmpcnt; + + if(strncmp(file, "../", 3) == 0 || strstr(file, "/../") || + strlen(file) >= sizeof(buf) - strlen("1997/0204/netlib/pub/0")) + return; + limit = 50; + if(diffb){ + limit = 10; + // create two tmp files for gunzip + for(i = 0, tmpcnt = 0; i < 2 && tmpcnt < 20; tmpcnt++){ + snprint(tmpf[i], sizeof(tmpf[0]), "/tmp/d%x", tmpcnt); + if(access(buf, AEXIST) == 0) + continue; + fd = create(tmpf[i], OWRITE, 0666); + if(fd < 0) + goto done; + close(fd); + i++; + } + } + otime = time(0); + hprint(HO,"<UL>\n"); + while(limit--){ + lastbefore(otime, file, buf); + dir = dirstat(buf); + if(dir == nil) + goto done; + dt = DAY/2; + while(otime <= dir->mtime){ + lastbefore(otime-dt, file, buf); + free(dir); + dir = dirstat(buf); + if(dir == nil) + goto done; + dt += DAY/2; + } + f = pair[toggle]; + strcpy(f, buf); + if(diffb && strcmp(f+strlen(f)-3, ".gz") == 0){ + gunzip(f, tmpf[toggle]); + strcpy(f, tmpf[toggle]); + } + if(diffb && started){ + hprint(HO, "<PRE>\n"); + hflush(HO); + switch(fork()){ + case 0: + execl("/bin/diff", "diff", "-nb", + pair[1-toggle], pair[toggle], nil); + hprint(HO, "can't exec diff: %r\n"); + break; + case -1: + hprint(HO, "fork failed: %r\n"); + break; + default: + while(waitpid() != -1) + ; + break; + } + hprint(HO, "</PRE>\n"); + } + hprint(HO,"<LI><A HREF=\"/historic/%s\">%s</A> %lld bytes\n", + buf, 4+asctime(gmtime(dir->mtime)), dir->length); + if(diffb) + hprint(HO," <FONT SIZE=-1>(%s)</FONT>\n", pair[toggle]); + toggle = 1-toggle; + started = 1; + otime = dir->mtime; + free(dir); + } + hprint(HO,"<LI>...\n"); +done: + hprint(HO,"</UL>\n"); + if(diffb){ + remove(tmpf[0]); + remove(tmpf[1]); + } +} + +int +send(HConnect *c) +{ + char *file, *s; + HSPairs *q; + + 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, nil); + if(c->req.search == nil || !*c->req.search) + return hfail(c, HNoData, "netlib_history"); + s = c->req.search; + while((s = strchr(s, '+')) != nil) + *s++ = ' '; + file = nil; + for(q = hparsequery(c, hstrdup(c, c->req.search)); q; q = q->next){ + if(strcmp(q->s, "file") == 0) + file = q->t; + else if(strcmp(q->s, "diff") == 0) + diffb = 1; + } + if(file == nil) + return hfail(c, HNoData, "netlib_history missing file field"); + logit(c, "netlib_hist %s%s", file, diffb?" DIFF":""); + + if(c->req.vermaj){ + hokheaders(c); + hprint(HO, "Content-type: text/html\r\n"); + hprint(HO, "\r\n"); + } + if(strcmp(c->req.meth, "HEAD") == 0){ + writelog(c, "Reply: 200 netlib_history 0\n"); + hflush(HO); + exits(nil); + } + + hprint(HO, "<HEAD><TITLE>%s history</TITLE></HEAD>\n<BODY>\n",file); + hprint(HO, "<H2>%s history</H2>\n",file); + hprint(HO, "<I>Netlib's copy of %s was changed\n", file); + hprint(HO, "on the dates shown. <BR>Click on the date link\n"); + hprint(HO, "to retrieve the corresponding version.</I>\n"); + if(diffb){ + hprint(HO, "<BR><I>Lines beginning with < are for the\n"); + hprint(HO, "newer of the two versions.</I>\n"); + } + + if(chdir("/usr/web/historic") < 0) + hprint(HO, "chdir failed: %r\n"); + netlibhistory(file); + + hprint(HO, "<BR><A HREF=\"http://cm.bell-labs.com/who/ehg\">Eric Grosse</A>\n"); + hprint(HO, "</BODY></HTML>\n"); + hflush(HO); + writelog(c, "Reply: 200 netlib_history %ld %ld\n", HO->seek, HO->seek); + return 1; +} + +void +main(int argc, char **argv) +{ + HConnect *c; + + c = init(argc, argv); + HO = &c->hout; + if(hparseheaders(c, HSTIMEOUT) >= 0) + send(c); + exits(nil); +} diff --git a/sys/src/cmd/ip/httpd/redirect.c b/sys/src/cmd/ip/httpd/redirect.c new file mode 100755 index 000000000..d7626452e --- /dev/null +++ b/sys/src/cmd/ip/httpd/redirect.c @@ -0,0 +1,227 @@ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +enum +{ + HASHSIZE = 1019, +}; + +typedef struct Redir Redir; +struct Redir +{ + Redir *next; + char *pat; + char *repl; + uint flags; /* generated from repl's decorations */ +}; + +static Redir *redirtab[HASHSIZE]; +static Redir *vhosttab[HASHSIZE]; +static char emptystring[1]; +/* these two arrays must be kept in sync */ +static char decorations[] = { Modsilent, Modperm, Modsubord, Modonly, '\0' }; +static uint redirflags[] = { Redirsilent, Redirperm, Redirsubord, Redironly, }; + +/* replacement field decorated with redirection modifiers? */ +static int +isdecorated(char *repl) +{ + return strchr(decorations, repl[0]) != nil; +} + +static uint +decor2flags(char *repl) +{ + uint flags; + char *p; + + flags = 0; + while ((p = strchr(decorations, *repl++)) != nil) + flags |= redirflags[p - decorations]; + return flags; +} + +/* return replacement without redirection modifiers */ +char * +undecorated(char *repl) +{ + while (isdecorated(repl)) + repl++; + return repl; +} + +static int +hashasu(char *key, int n) +{ + ulong h; + + h = 0; + while(*key != 0) + h = 65599*h + *(uchar*)key++; + return h % n; +} + +static void +insert(Redir **tab, char *pat, char *repl) +{ + Redir **l; + Redir *srch; + ulong hash; + + hash = hashasu(pat, HASHSIZE); + for(l = &tab[hash]; *l; l = &(*l)->next) + ; + *l = srch = ezalloc(sizeof(Redir)); + srch->pat = pat; + srch->flags = decor2flags(repl); + srch->repl = undecorated(repl); + srch->next = 0; +} + +static void +cleartab(Redir **tab) +{ + Redir *t; + int i; + + for(i = 0; i < HASHSIZE; i++){ + while((t = tab[i]) != nil){ + tab[i] = t->next; + free(t->pat); + free(t->repl); + free(t); + } + } +} + +void +redirectinit(void) +{ + static Biobuf *b = nil; + static Qid qid; + char *file, *line, *s, *host, *field[3]; + static char pfx[] = "http://"; + + file = "/sys/lib/httpd.rewrite"; + if(b != nil){ + if(updateQid(Bfildes(b), &qid) == 0) + return; + Bterm(b); + } + b = Bopen(file, OREAD); + if(b == nil) + sysfatal("can't read from %s", file); + updateQid(Bfildes(b), &qid); + + cleartab(redirtab); + cleartab(vhosttab); + + while((line = Brdline(b, '\n')) != nil){ + line[Blinelen(b)-1] = 0; + s = strchr(line, '#'); + if(s != nil && (s == line || s[-1] == ' ' || s[-1] == '\t')) + *s = '\0'; /* chop comment iff after whitespace */ + if(tokenize(line, field, nelem(field)) == 2){ + if(strncmp(field[0], pfx, STRLEN(pfx)) == 0 && + strncmp(undecorated(field[1]), pfx, STRLEN(pfx)) != 0){ + /* url -> filename */ + host = field[0] + STRLEN(pfx); + s = strrchr(host, '/'); + if(s) + *s = 0; /* chop trailing slash */ + + insert(vhosttab, estrdup(host), estrdup(field[1])); + }else{ + insert(redirtab, estrdup(field[0]), estrdup(field[1])); + } + } + } + syslog(0, HTTPLOG, "redirectinit pid=%d", getpid()); +} + +static Redir* +lookup(Redir **tab, char *pat, int count) +{ + Redir *srch; + ulong hash; + + hash = hashasu(pat,HASHSIZE); + for(srch = tab[hash]; srch != nil; srch = srch->next) + if(strcmp(pat, srch->pat) == 0) { + /* only exact match wanted? */ + if (!(srch->flags & Redironly) || count == 0) + return srch; + } + return nil; +} + +static char* +prevslash(char *p, char *s) +{ + while(--s > p) + if(*s == '/') + break; + return s; +} + +/* + * find the longest match of path against the redirection table, + * chopping off the rightmost path component until success or + * there's nothing left. return a copy of the replacement string + * concatenated with a slash and the portion of the path *not* matched. + * So a match of /who/gre/some/stuff.html matched against + * /who/gre http://gremlinsrus.org + * returns + * http://gremlinsrus.org/some/stuff.html + * + * further flags: if Redironly, match only the named page and no + * subordinate ones. if Redirsubord, map the named patch and any + * subordinate ones to the same replacement URL. + */ +char* +redirect(HConnect *hc, char *path, uint *flagp) +{ + Redir *redir; + char *s, *newpath, *repl; + int c, n, count; + + count = 0; + for(s = strchr(path, '\0'); s > path; s = prevslash(path, s)){ + c = *s; + *s = '\0'; + redir = lookup(redirtab, path, count++); + *s = c; + if(redir != nil){ + if (flagp) + *flagp = redir->flags; + repl = redir->repl; + if(redir->flags & Redirsubord) + /* don't append s, all matches map to repl */ + s = ""; + n = strlen(repl) + strlen(s) + 2 + UTFmax; + newpath = halloc(hc, n); + snprint(newpath, n, "%s%s", repl, s); + return newpath; + } + } + return nil; +} + +/* + * if host is virtual, return implicit prefix for URI within webroot. + * if not, return empty string. + * return value should not be freed by caller. + */ +char* +masquerade(char *host) +{ + Redir *redir; + + redir = lookup(vhosttab, host, 0); + if(redir == nil) + return emptystring; + return redir->repl; +} diff --git a/sys/src/cmd/ip/httpd/save.c b/sys/src/cmd/ip/httpd/save.c new file mode 100755 index 000000000..d80323ae1 --- /dev/null +++ b/sys/src/cmd/ip/httpd/save.c @@ -0,0 +1,154 @@ +/* + * for GET or POST to /magic/save/foo. + * add incoming data to foo.data. + * send foo.html as reply. + * + * supports foo.data with "exclusive use" mode to prevent interleaved saves. + * thus http://cm.bell-labs.com/magic/save/t?args should access: + * -lrw-rw--w- M 21470 ehg web 1533 May 21 18:19 /usr/web/save/t.data + * --rw-rw-r-- M 21470 ehg web 73 May 21 18:17 /usr/web/save/t.html +*/ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +enum +{ + MaxLog = 24*1024, /* limit on length of any one log request */ + LockSecs = MaxLog/500, /* seconds to wait before giving up on opening the data file */ +}; + +static int +dangerous(char *s) +{ + if(s == nil) + return 1; + + /* + * This check shouldn't be needed; + * filename folding is already supposed to have happened. + * But I'm paranoid. + */ + while(s = strchr(s,'/')){ + if(s[1]=='.' && s[2]=='.') + return 1; + s++; + } + return 0; +} + +/* + * open a file which might be locked. + * if it is, spin until available + */ +int +openLocked(char *file, int mode) +{ + char buf[ERRMAX]; + int tries, fd; + + for(tries = 0; tries < LockSecs*2; tries++){ + fd = open(file, mode); + if(fd >= 0) + return fd; + errstr(buf, sizeof buf); + if(strstr(buf, "locked") == nil) + break; + sleep(500); + } + return -1; +} + +void +main(int argc, char **argv) +{ + HConnect *c; + Dir *dir; + Hio *hin, *hout; + char *s, *t, *fn; + int n, nfn, datafd, htmlfd; + + c = init(argc, argv); + + if(dangerous(c->req.uri)){ + hfail(c, HSyntax); + exits("failed"); + } + + if(hparseheaders(c, HSTIMEOUT) < 0) + exits("failed"); + hout = &c->hout; + if(c->head.expectother){ + hfail(c, HExpectFail, nil); + exits("failed"); + } + if(c->head.expectcont){ + hprint(hout, "100 Continue\r\n"); + hprint(hout, "\r\n"); + hflush(hout); + } + + s = nil; + if(strcmp(c->req.meth, "POST") == 0){ + hin = hbodypush(&c->hin, c->head.contlen, c->head.transenc); + if(hin != nil){ + alarm(HSTIMEOUT); + s = hreadbuf(hin, hin->pos); + alarm(0); + } + if(s == nil){ + hfail(c, HBadReq, nil); + exits("failed"); + } + t = strchr(s, '\n'); + if(t != nil) + *t = '\0'; + }else if(strcmp(c->req.meth, "GET") != 0 && strcmp(c->req.meth, "HEAD") != 0){ + hunallowed(c, "GET, HEAD, PUT"); + exits("unallowed"); + }else + s = c->req.search; + if(s == nil){ + hfail(c, HNoData, "save"); + exits("failed"); + } + + if(strlen(s) > MaxLog) + s[MaxLog] = '\0'; + n = snprint(c->xferbuf, HBufSize, "at %ld %s\n", time(0), s); + + + nfn = strlen(c->req.uri) + 64; + fn = halloc(c, nfn); + + /* + * open file descriptors & write log line + */ + snprint(fn, nfn, "/usr/web/save/%s.html", c->req.uri); + htmlfd = open(fn, OREAD); + if(htmlfd < 0 || (dir = dirfstat(htmlfd)) == nil){ + hfail(c, HNotFound, c->req.uri); + exits("failed"); + return; + } + + snprint(fn, nfn, "/usr/web/save/%s.data", c->req.uri); + datafd = openLocked(fn, OWRITE); + if(datafd < 0){ + errstr(c->xferbuf, sizeof c->xferbuf); + if(strstr(c->xferbuf, "locked") != nil) + hfail(c, HTempFail, c->req.uri); + else + hfail(c, HNotFound, c->req.uri); + exits("failed"); + } + seek(datafd, 0, 2); + write(datafd, c->xferbuf, n); + close(datafd); + + sendfd(c, htmlfd, dir, hmkcontent(c, "text", "html", nil), nil); + + exits(nil); +} diff --git a/sys/src/cmd/ip/httpd/sendfd.c b/sys/src/cmd/ip/httpd/sendfd.c new file mode 100755 index 000000000..6ae4e7d73 --- /dev/null +++ b/sys/src/cmd/ip/httpd/sendfd.c @@ -0,0 +1,462 @@ +#include <u.h> +#include <libc.h> +#include <auth.h> +#include "httpd.h" +#include "httpsrv.h" + +static void printtype(Hio *hout, HContent *type, HContent *enc); + +/* + * these should be done better; see the reponse codes in /lib/rfc/rfc2616 for + * more info on what should be included. + */ +#define UNAUTHED "You are not authorized to see this area.\n" +#define NOCONTENT "No acceptable type of data is available.\n" +#define NOENCODE "No acceptable encoding of the contents is available.\n" +#define UNMATCHED "The entity requested does not match the existing entity.\n" +#define BADRANGE "No bytes are avaible for the range you requested.\n" + +/* + * fd references a file which has been authorized & checked for relocations. + * send back the headers & its contents. + * includes checks for conditional requests & ranges. + */ +int +sendfd(HConnect *c, int fd, Dir *dir, HContent *type, HContent *enc) +{ + Qid qid; + HRange *r; + HContents conts; + Hio *hout; + char *boundary, etag[32]; + long mtime; + ulong tr; + int n, nw, multir, ok; + vlong wrote, length; + + hout = &c->hout; + length = dir->length; + mtime = dir->mtime; + qid = dir->qid; + free(dir); + + /* + * figure out the type of file and send headers + */ + n = -1; + r = nil; + multir = 0; + boundary = nil; + if(c->req.vermaj){ + if(type == nil && enc == nil){ + conts = uriclass(c, c->req.uri); + type = conts.type; + enc = conts.encoding; + if(type == nil && enc == nil){ + n = read(fd, c->xferbuf, HBufSize-1); + if(n > 0){ + c->xferbuf[n] = '\0'; + conts = dataclass(c, c->xferbuf, n); + type = conts.type; + enc = conts.encoding; + } + } + } + if(type == nil) + type = hmkcontent(c, "application", "octet-stream", nil); + + snprint(etag, sizeof(etag), "\"%lluxv%lux\"", qid.path, qid.vers); + ok = checkreq(c, type, enc, mtime, etag); + if(ok <= 0){ + close(fd); + return ok; + } + + /* + * check for if-range requests + */ + if(c->head.range == nil + || c->head.ifrangeetag != nil && !etagmatch(1, c->head.ifrangeetag, etag) + || c->head.ifrangedate != 0 && c->head.ifrangedate != mtime){ + c->head.range = nil; + c->head.ifrangeetag = nil; + c->head.ifrangedate = 0; + } + + if(c->head.range != nil){ + c->head.range = fixrange(c->head.range, length); + if(c->head.range == nil){ + if(c->head.ifrangeetag == nil && c->head.ifrangedate == 0){ + hprint(hout, "%s 416 Request range not satisfiable\r\n", hversion); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Content-Range: bytes */%lld\r\n", length); + hprint(hout, "Content-Length: %d\r\n", STRLEN(BADRANGE)); + hprint(hout, "Content-Type: text/html\r\n"); + if(c->head.closeit) + hprint(hout, "Connection: close\r\n"); + else if(!http11(c)) + hprint(hout, "Connection: Keep-Alive\r\n"); + hprint(hout, "\r\n"); + if(strcmp(c->req.meth, "HEAD") != 0) + hprint(hout, "%s", BADRANGE); + hflush(hout); + writelog(c, "Reply: 416 Request range not satisfiable\n"); + close(fd); + return 1; + } + c->head.ifrangeetag = nil; + c->head.ifrangedate = 0; + } + } + if(c->head.range == nil) + hprint(hout, "%s 200 OK\r\n", hversion); + else + hprint(hout, "%s 206 Partial Content\r\n", hversion); + + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "ETag: %s\r\n", etag); + + /* + * can't send some entity headers if partially responding + * to an if-range: etag request + */ + r = c->head.range; + if(r == nil) + hprint(hout, "Content-Length: %lld\r\n", length); + else if(r->next == nil){ + hprint(hout, "Content-Range: bytes %ld-%ld/%lld\r\n", r->start, r->stop, length); + hprint(hout, "Content-Length: %ld\r\n", r->stop - r->start); + }else{ + multir = 1; + boundary = hmkmimeboundary(c); + hprint(hout, "Content-Type: multipart/byteranges; boundary=%s\r\n", boundary); + } + if(c->head.ifrangeetag == nil){ + hprint(hout, "Last-Modified: %D\r\n", mtime); + if(!multir) + printtype(hout, type, enc); + if(c->head.fresh_thresh) + hintprint(c, hout, c->req.uri, c->head.fresh_thresh, c->head.fresh_have); + } + + if(c->head.closeit) + hprint(hout, "Connection: close\r\n"); + else if(!http11(c)) + hprint(hout, "Connection: Keep-Alive\r\n"); + hprint(hout, "\r\n"); + } + if(strcmp(c->req.meth, "HEAD") == 0){ + if(c->head.range == nil) + writelog(c, "Reply: 200 file 0\n"); + else + writelog(c, "Reply: 206 file 0\n"); + hflush(hout); + close(fd); + return 1; + } + + /* + * send the file if it's a normal file + */ + if(r == nil){ + hflush(hout); + + wrote = 0; + if(n > 0) + wrote = write(hout->fd, c->xferbuf, n); + if(n <= 0 || wrote == n){ + while((n = read(fd, c->xferbuf, HBufSize)) > 0){ + nw = write(hout->fd, c->xferbuf, n); + if(nw != n){ + if(nw > 0) + wrote += nw; + break; + } + wrote += nw; + } + } + writelog(c, "Reply: 200 file %lld %lld\n", length, wrote); + close(fd); + if(length == wrote) + return 1; + return -1; + } + + /* + * for multipart/byterange messages, + * it is not ok for the boundary string to appear within a message part. + * however, it probably doesn't matter, since there are lengths for every part. + */ + wrote = 0; + ok = 1; + for(; r != nil; r = r->next){ + if(multir){ + hprint(hout, "\r\n--%s\r\n", boundary); + printtype(hout, type, enc); + hprint(hout, "Content-Range: bytes %ld-%ld/%lld\r\n", r->start, r->stop, length); + hprint(hout, "Content-Length: %ld\r\n", r->stop - r->start); + hprint(hout, "\r\n"); + } + hflush(hout); + + if(seek(fd, r->start, 0) != r->start){ + ok = -1; + break; + } + for(tr = r->stop - r->start + 1; tr; tr -= n){ + n = tr; + if(n > HBufSize) + n = HBufSize; + if(read(fd, c->xferbuf, n) != n){ + ok = -1; + goto breakout; + } + nw = write(hout->fd, c->xferbuf, n); + if(nw != n){ + if(nw > 0) + wrote += nw; + ok = -1; + goto breakout; + } + wrote += nw; + } + } +breakout:; + if(r == nil){ + if(multir){ + hprint(hout, "--%s--\r\n", boundary); + hflush(hout); + } + writelog(c, "Reply: 206 partial content %lld %lld\n", length, wrote); + }else + writelog(c, "Reply: 206 partial content, early termination %lld %lld\n", length, wrote); + close(fd); + return ok; +} + +static void +printtype(Hio *hout, HContent *type, HContent *enc) +{ + hprint(hout, "Content-Type: %s/%s", type->generic, type->specific); +/* + if(cistrcmp(type->generic, "text") == 0) + hprint(hout, ";charset=utf-8"); +*/ + hprint(hout, "\r\n"); + if(enc != nil) + hprint(hout, "Content-Encoding: %s\r\n", enc->generic); +} + +int +etagmatch(int strong, HETag *tags, char *e) +{ + char *s, *t; + + for(; tags != nil; tags = tags->next){ + if(strong && tags->weak) + continue; + s = tags->etag; + if(s[0] == '*' && s[1] == '\0') + return 1; + + t = e + 1; + while(*t != '"'){ + if(*s != *t) + break; + s++; + t++; + } + + if(*s == '\0' && *t == '"') + return 1; + } + return 0; +} + +static char * +acceptcont(char *s, char *e, HContent *ok, char *which) +{ + char *sep; + + if(ok == nil) + return seprint(s, e, "Your browser accepts any %s.<br>\n", which); + s = seprint(s, e, "Your browser accepts %s: ", which); + sep = ""; + for(; ok != nil; ok = ok->next){ + if(ok->specific) + s = seprint(s, e, "%s%s/%s", sep, ok->generic, ok->specific); + else + s = seprint(s, e, "%s%s", sep, ok->generic); + sep = ", "; + } + return seprint(s, e, ".<br>\n"); +} + +/* + * send back a nice error message if the content is unacceptable + * to get this message in ie, go to tools, internet options, advanced, + * and turn off Show Friendly HTTP Error Messages under the Browsing category + */ +static int +notaccept(HConnect *c, HContent *type, HContent *enc, char *which) +{ + Hio *hout; + char *s, *e; + + hout = &c->hout; + e = &c->xferbuf[HBufSize]; + s = c->xferbuf; + s = seprint(s, e, "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">\n"); + s = seprint(s, e, "<html>\n<title>Unacceptable %s</title>\n<body>\n", which); + s = seprint(s, e, "Your browser will not accept this data, %H, because of its %s.<br>\n", c->req.uri, which); + s = seprint(s, e, "Its Content-Type is %s/%s", type->generic, type->specific); + if(enc != nil) + s = seprint(s, e, ", and Content-Encoding is %s", enc->generic); + s = seprint(s, e, ".<br>\n\n"); + + s = acceptcont(s, e, c->head.oktype, "Content-Type"); + s = acceptcont(s, e, c->head.okencode, "Content-Encoding"); + s = seprint(s, e, "</body>\n</html>\n"); + + hprint(hout, "%s 406 Not Acceptable\r\n", hversion); + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "Content-Type: text/html\r\n"); + hprint(hout, "Content-Length: %lud\r\n", s - c->xferbuf); + if(c->head.closeit) + hprint(hout, "Connection: close\r\n"); + else if(!http11(c)) + hprint(hout, "Connection: Keep-Alive\r\n"); + hprint(hout, "\r\n"); + if(strcmp(c->req.meth, "HEAD") != 0) + hwrite(hout, c->xferbuf, s - c->xferbuf); + writelog(c, "Reply: 406 Not Acceptable\nReason: %s\n", which); + return hflush(hout); +} + +/* + * check time and entity tag conditions. + */ +int +checkreq(HConnect *c, HContent *type, HContent *enc, long mtime, char *etag) +{ + Hio *hout; + int m; + + hout = &c->hout; + if(c->req.vermaj >= 1 && c->req.vermin >= 1 && !hcheckcontent(type, c->head.oktype, "Content-Type", 0)) + return notaccept(c, type, enc, "Content-Type"); + if(c->req.vermaj >= 1 && c->req.vermin >= 1 && !hcheckcontent(enc, c->head.okencode, "Content-Encoding", 0)) + return notaccept(c, type, enc, "Content-Encoding"); + + /* + * can use weak match only with get or head; + * this always uses strong matches + */ + m = etagmatch(1, c->head.ifnomatch, etag); + + if(m && strcmp(c->req.meth, "GET") != 0 && strcmp(c->req.meth, "HEAD") != 0 + || c->head.ifunmodsince && c->head.ifunmodsince < mtime + || c->head.ifmatch != nil && !etagmatch(1, c->head.ifmatch, etag)){ + hprint(hout, "%s 412 Precondition Failed\r\n", hversion); + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "Content-Type: text/html\r\n"); + hprint(hout, "Content-Length: %d\r\n", STRLEN(UNMATCHED)); + if(c->head.closeit) + hprint(hout, "Connection: close\r\n"); + else if(!http11(c)) + hprint(hout, "Connection: Keep-Alive\r\n"); + hprint(hout, "\r\n"); + if(strcmp(c->req.meth, "HEAD") != 0) + hprint(hout, "%s", UNMATCHED); + writelog(c, "Reply: 412 Precondition Failed\n"); + return hflush(hout); + } + + if(c->head.ifmodsince >= mtime + && (m || c->head.ifnomatch == nil)){ + /* + * can only send back Date, ETag, Content-Location, + * Expires, Cache-Control, and Vary entity-headers + */ + hprint(hout, "%s 304 Not Modified\r\n", hversion); + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "ETag: %s\r\n", etag); + if(c->head.closeit) + hprint(hout, "Connection: close\r\n"); + else if(!http11(c)) + hprint(hout, "Connection: Keep-Alive\r\n"); + hprint(hout, "\r\n"); + writelog(c, "Reply: 304 Not Modified\n"); + return hflush(hout); + } + return 1; +} + +/* + * length is the actual length of the entity requested. + * discard any range requests which are invalid, + * ie start after the end, or have stop before start. + * rewrite suffix requests + */ +HRange* +fixrange(HRange *h, long length) +{ + HRange *r, *rr; + + if(length == 0) + return nil; + + /* + * rewrite each range to reflect the actual length of the file + * toss out any invalid ranges + */ + rr = nil; + for(r = h; r != nil; r = r->next){ + if(r->suffix){ + r->start = length - r->stop; + if(r->start >= length) + r->start = 0; + r->stop = length - 1; + r->suffix = 0; + } + if(r->stop >= length) + r->stop = length - 1; + if(r->start > r->stop){ + if(rr == nil) + h = r->next; + else + rr->next = r->next; + }else + rr = r; + } + + /* + * merge consecutive overlapping or abutting ranges + * + * not clear from rfc2616 how much merging needs to be done. + * this code merges only if a range is adjacent to a later starting, + * over overlapping or abutting range. this allows a client + * to request wanted data first, followed by other data. + * this may be useful then fetching part of a page, then the adjacent regions. + */ + if(h == nil) + return h; + r = h; + for(;;){ + rr = r->next; + if(rr == nil) + break; + if(r->start <= rr->start && r->stop + 1 >= rr->start){ + if(r->stop < rr->stop) + r->stop = rr->stop; + r->next = rr->next; + }else + r = rr; + } + return h; +} diff --git a/sys/src/cmd/ip/httpd/webls.c b/sys/src/cmd/ip/httpd/webls.c new file mode 100755 index 000000000..0da5d5e68 --- /dev/null +++ b/sys/src/cmd/ip/httpd/webls.c @@ -0,0 +1,335 @@ +#include <u.h> +#include <libc.h> +#include <ctype.h> +#include <bio.h> +#include <regexp.h> +#include <fcall.h> +#include "httpd.h" +#include "httpsrv.h" + +static Hio *hout; +static Hio houtb; +static HConnect *connect; +static int vermaj, gidwidth, uidwidth, lenwidth, devwidth; +static Biobuf *aio, *dio; + +static void +doctype(void) +{ + hprint(hout, "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"); + hprint(hout, " \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n"); +} + +void +error(char *title, char *fmt, ...) +{ + va_list arg; + char buf[1024], *out; + + va_start(arg, fmt); + out = vseprint(buf, buf+sizeof(buf), fmt, arg); + va_end(arg); + *out = 0; + + hprint(hout, "%s 404 %s\r\n", hversion, title); + hprint(hout, "Date: %D\r\n", time(nil)); + hprint(hout, "Server: Plan9\r\n"); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + doctype(); + hprint(hout, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"); + hprint(hout, "<head><title>%s</title></head>\n", title); + hprint(hout, "<body>\n"); + hprint(hout, "<h1>%s</h1>\n", title); + hprint(hout, "%s\n", buf); + hprint(hout, "</body>\n"); + hprint(hout, "</html>\n"); + hflush(hout); + writelog(connect, "Reply: 404\nReason: %s\n", title); + exits(nil); +} + +/* + * Are we actually allowed to look in here? + * + * Rules: + * 1) If neither allowed nor denied files exist, access is granted. + * 2) If allowed exists and denied does not, dir *must* be in allowed + * for access to be granted, otherwise, access is denied. + * 3) If denied exists and allowed does not, dir *must not* be in + * denied for access to be granted, otherwise, access is enied. + * 4) If both exist, okay if either (a) file is not in denied, or + * (b) in denied and in allowed. Otherwise, access is denied. + */ +static Reprog * +getre(Biobuf *buf) +{ + Reprog *re; + char *p, *t; + char *bbuf; + int n; + + if (buf == nil) + return(nil); + for ( ; ; free(p)) { + p = Brdstr(buf, '\n', 0); + if (p == nil) + return(nil); + t = strchr(p, '#'); + if (t != nil) + *t = '\0'; + t = p + strlen(p); + while (--t > p && isspace(*t)) + *t = '\0'; + n = strlen(p); + if (n == 0) + continue; + + /* root the regular expresssion */ + bbuf = malloc(n+2); + if(bbuf == nil) + sysfatal("out of memory"); + bbuf[0] = '^'; + strcpy(bbuf+1, p); + re = regcomp(bbuf); + free(bbuf); + + if (re == nil) + continue; + free(p); + return(re); + } +} + +static int +allowed(char *dir) +{ + Reprog *re; + int okay; + Resub match; + + if (strcmp(dir, "..") == 0 || strncmp(dir, "../", 3) == 0) + return(0); + if (aio == nil) + return(0); + + if (aio != nil) + Bseek(aio, 0, 0); + if (dio != nil) + Bseek(dio, 0, 0); + + /* if no deny list, assume everything is denied */ + okay = (dio != nil); + + /* go through denials till we find a match */ + while (okay && (re = getre(dio)) != nil) { + memset(&match, 0, sizeof(match)); + okay = (regexec(re, dir, &match, 1) != 1); + free(re); + } + + /* go through accepts till we have a match */ + if (aio == nil) + return(okay); + while (!okay && (re = getre(aio)) != nil) { + memset(&match, 0, sizeof(match)); + okay = (regexec(re, dir, &match, 1) == 1); + free(re); + } + return(okay); +} + +/* + * Comparison routine for sorting the directory. + */ +static int +compar(Dir *a, Dir *b) +{ + return(strcmp(a->name, b->name)); +} + +/* + * These is for formating; how wide are variable-length + * fields? + */ +static void +maxwidths(Dir *dp, long n) +{ + long i; + char scratch[64]; + + for (i = 0; i < n; i++) { + if (snprint(scratch, sizeof scratch, "%ud", dp[i].dev) > devwidth) + devwidth = strlen(scratch); + if (strlen(dp[i].uid) > uidwidth) + uidwidth = strlen(dp[i].uid); + if (strlen(dp[i].gid) > gidwidth) + gidwidth = strlen(dp[i].gid); + if (snprint(scratch, sizeof scratch, "%lld", dp[i].length) > lenwidth) + lenwidth = strlen(scratch); + } +} + +/* + * Do an actual directory listing. + * asciitime is lifted directly out of ls. + */ +char * +asciitime(long l) +{ + ulong clk; + static char buf[32]; + char *t; + + clk = time(nil); + t = ctime(l); + /* 6 months in the past or a day in the future */ + if(l<clk-180L*24*60*60 || clk+24L*60*60<l){ + memmove(buf, t+4, 7); /* month and day */ + memmove(buf+7, t+23, 5); /* year */ + }else + memmove(buf, t+4, 12); /* skip day of week */ + buf[12] = 0; + return buf; +} + +static void +dols(char *dir) +{ + Dir *d; + char *f, *p,*nm; + long i, n; + int fd; + + cleanname(dir); // expands "" to "."; ``dir+1'' access below depends on that + if (!allowed(dir)) { + error("Permission denied", "<p>Cannot list directory %s: Access prohibited</p>", dir); + return; + } + fd = open(dir, OREAD); + if (fd < 0) { + error("Cannot read directory", "<p>Cannot read directory %s: %r</p>", dir); + return; + } + if (vermaj) { + hokheaders(connect); + hprint(hout, "Content-type: text/html\r\n"); + hprint(hout, "\r\n"); + } + doctype(); + hprint(hout, "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"); + hprint(hout, "<head><title>Index of %s</title></head>\n", dir); + hprint(hout, "<body>\n"); + hprint(hout, "<h1>Index of "); + nm = dir; + while((p = strchr(nm, '/')) != nil){ + *p = '\0'; + f = (*dir == '\0') ? "/" : dir; + if (!(*dir == '\0' && *(dir+1) == '\0') && allowed(f)) + hprint(hout, "<a href=\"/magic/webls?dir=%H\">%s/</a>", f, nm); + else + hprint(hout, "%s/", nm); + *p = '/'; + nm = p+1; + } + hprint(hout, "%s</h1>\n", nm); + n = dirreadall(fd, &d); + close(fd); + maxwidths(d, n); + qsort(d, n, sizeof(Dir), (int (*)(void *, void *))compar); + hprint(hout, "<pre>\n"); + for (i = 0; i < n; i++) { + f = smprint("%s/%s", dir, d[i].name); + cleanname(f); + if (d[i].mode & DMDIR) { + p = smprint("/magic/webls?dir=%H", f); + free(f); + f = p; + } + hprint(hout, "%M %C %*ud %-*s %-*s %*lld %s <a href=\"%s\">%s</a>\n", + d[i].mode, d[i].type, + devwidth, d[i].dev, + uidwidth, d[i].uid, + gidwidth, d[i].gid, + lenwidth, d[i].length, + asciitime(d[i].mtime), f, d[i].name); + free(f); + } + f = smprint("%s/..", dir); + cleanname(f); + if (strcmp(f, dir) != 0 && allowed(f)) + hprint(hout, "\nGo to <a href=\"/magic/webls?dir=%H\">parent</a> directory\n", f); + else + hprint(hout, "\nEnd of directory listing\n"); + free(f); + hprint(hout, "</pre>\n</body>\n</html>\n"); + hflush(hout); + free(d); +} + +/* + * Handle unpacking the request in the URI and + * invoking the actual handler. + */ +static void +dosearch(char *search) +{ + if (strncmp(search, "dir=", 4) == 0){ + search = hurlunesc(connect, search+4); + dols(search); + return; + } + + /* + * Otherwise, we've gotten an illegal request. + * spit out a non-apologetic error. + */ + search = hurlunesc(connect, search); + error("Bad directory listing request", + "<p>Illegal formatted directory listing request:</p>\n" + "<p>%H</p>", search); +} + +void +main(int argc, char **argv) +{ + fmtinstall('H', httpfmt); + fmtinstall('U', hurlfmt); + fmtinstall('M', dirmodefmt); + + aio = Bopen("/sys/lib/webls.allowed", OREAD); + dio = Bopen("/sys/lib/webls.denied", OREAD); + + if(argc == 2){ + hinit(&houtb, 1, Hwrite); + hout = &houtb; + dols(argv[1]); + exits(nil); + } + close(2); + + connect = init(argc, argv); + hout = &connect->hout; + vermaj = connect->req.vermaj; + if(hparseheaders(connect, HSTIMEOUT) < 0) + exits("failed"); + + if(strcmp(connect->req.meth, "GET") != 0 && strcmp(connect->req.meth, "HEAD") != 0){ + hunallowed(connect, "GET, HEAD"); + exits("not allowed"); + } + if(connect->head.expectother || connect->head.expectcont){ + hfail(connect, HExpectFail, nil); + exits("failed"); + } + + bind(webroot, "/", MREPL); + + if(connect->req.search != nil) + dosearch(connect->req.search); + else + error("Bad argument", "<p>Need a search argument</p>"); + hflush(hout); + writelog(connect, "200 webls %ld %ld\n", hout->seek, hout->seek); + exits(nil); +} diff --git a/sys/src/cmd/ip/httpd/webls.denied b/sys/src/cmd/ip/httpd/webls.denied new file mode 100755 index 000000000..8d98f9deb --- /dev/null +++ b/sys/src/cmd/ip/httpd/webls.denied @@ -0,0 +1 @@ +.* diff --git a/sys/src/cmd/ip/httpd/wikipost.c b/sys/src/cmd/ip/httpd/wikipost.c new file mode 100755 index 000000000..31fbedda1 --- /dev/null +++ b/sys/src/cmd/ip/httpd/wikipost.c @@ -0,0 +1,305 @@ +/* + * Accept new wiki pages or modifications to existing ones via POST method. + * + * Talks to the server at /srv/wiki.service. + */ +#include <u.h> +#include <libc.h> +#include <bio.h> +#include "httpd.h" +#include "httpsrv.h" + +#define LOG "wiki" + +HConnect *hc; +HSPriv *hp; + + +/* go from possibly-latin1 url with escapes to utf */ +char * +_urlunesc(char *s) +{ + char *t, *v, *u; + Rune r; + int c, n; + + /* unescape */ + u = halloc(hc, strlen(s)+1); + for(t = u; c = *s; s++){ + if(c == '%'){ + n = s[1]; + if(n >= '0' && n <= '9') + n = n - '0'; + else if(n >= 'A' && n <= 'F') + n = n - 'A' + 10; + else if(n >= 'a' && n <= 'f') + n = n - 'a' + 10; + else + break; + r = n; + n = s[2]; + if(n >= '0' && n <= '9') + n = n - '0'; + else if(n >= 'A' && n <= 'F') + n = n - 'A' + 10; + else if(n >= 'a' && n <= 'f') + n = n - 'a' + 10; + else + break; + s += 2; + c = r*16+n; + } + *t++ = c; + } + *t = 0; + + /* latin1 heuristic */ + v = halloc(hc, UTFmax*strlen(u) + 1); + s = u; + t = v; + while(*s){ + /* in decoding error, assume latin1 */ + if((n=chartorune(&r, s)) == 1 && r == 0x80) + r = *s; + s += n; + t += runetochar(t, &r); + } + *t = 0; + + return v; +} + +enum +{ + MaxLog = 100*1024, /* limit on length of any one log request */ +}; + +static int +dangerous(char *s) +{ + if(s == nil) + return 1; + + /* + * This check shouldn't be needed; + * filename folding is already supposed to have happened. + * But I'm paranoid. + */ + while(s = strchr(s,'/')){ + if(s[1]=='.' && s[2]=='.') + return 1; + s++; + } + return 0; +} + +char* +unhttp(char *s) +{ + char *p, *r, *w; + + if(s == nil) + return nil; + + for(p=s; *p; p++) + if(*p=='+') + *p = ' '; + s = _urlunesc(s); + + for(r=w=s; *r; r++){ + if(*r != '\r') + *w++ = *r; + } + *w = '\0'; + return s; +} + +void +mountwiki(HConnect *c, char *service) +{ + char buf[128]; + int fd; + + /* already in (possibly private) namespace? */ + snprint(buf, sizeof buf, "/mnt/wiki.%s/new", service); + if (access(buf, AREAD) == 0){ + if (bind(buf, "/mnt/wiki", MREPL) < 0){ + syslog(0, LOG, "%s bind /mnt/wiki failed: %r", + hp->remotesys); + hfail(c, HNotFound); + exits("bind /mnt/wiki failed"); + } + return; + } + + /* old way: public wikifs from /srv */ + snprint(buf, sizeof buf, "/srv/wiki.%s", service); + if((fd = open(buf, ORDWR)) < 0){ + syslog(0, LOG, "%s open %s failed: %r", buf, hp->remotesys); + hfail(c, HNotFound); + exits("failed"); + } + if(mount(fd, -1, "/mnt/wiki", MREPL, "") < 0){ + syslog(0, LOG, "%s mount /mnt/wiki failed: %r", hp->remotesys); + hfail(c, HNotFound); + exits("failed"); + } + close(fd); +} + +char* +dowiki(HConnect *c, char *title, char *author, char *comment, char *base, ulong version, char *text) +{ + int fd, l, n, err; + char *p, tmp[256]; +int i; + + if((fd = open("/mnt/wiki/new", ORDWR)) < 0){ + syslog(0, LOG, "%s open /mnt/wiki/new failed: %r", hp->remotesys); + hfail(c, HNotFound); + exits("failed"); + } + +i=0; + if((i++,fprint(fd, "%s\nD%lud\nA%s (%s)\n", title, version, author, hp->remotesys) < 0) + || (i++,(comment && comment[0] && fprint(fd, "C%s\n", comment) < 0)) + || (i++,fprint(fd, "\n") < 0) + || (i++,(text[0] && write(fd, text, strlen(text)) != strlen(text)))){ + syslog(0, LOG, "%s write failed %d %ld fd %d: %r", hp->remotesys, i, strlen(text), fd); + hfail(c, HInternal); + exits("failed"); + } + + err = write(fd, "", 0); + if(err) + syslog(0, LOG, "%s commit failed %d: %r", hp->remotesys, err); + + seek(fd, 0, 0); + if((n = read(fd, tmp, sizeof(tmp)-1)) <= 0){ + if(n == 0) + werrstr("short read"); + syslog(0, LOG, "%s read failed: %r", hp->remotesys); + hfail(c, HInternal); + exits("failed"); + } + + tmp[n] = '\0'; + + p = halloc(c, l=strlen(base)+strlen(tmp)+40); + snprint(p, l, "%s/%s/%s.html", base, tmp, err ? "werror" : "index"); + return p; +} + + +void +main(int argc, char **argv) +{ + Hio *hin, *hout; + char *s, *t, *p, *f[10]; + char *text, *title, *service, *base, *author, *comment, *url; + int i, nf; + ulong version; + + hc = init(argc, argv); + hp = hc->private; + + if(dangerous(hc->req.uri)){ + hfail(hc, HSyntax); + exits("failed"); + } + + if(hparseheaders(hc, HSTIMEOUT) < 0) + exits("failed"); + hout = &hc->hout; + if(hc->head.expectother){ + hfail(hc, HExpectFail, nil); + exits("failed"); + } + if(hc->head.expectcont){ + hprint(hout, "100 Continue\r\n"); + hprint(hout, "\r\n"); + hflush(hout); + } + + s = nil; + if(strcmp(hc->req.meth, "POST") == 0){ + hin = hbodypush(&hc->hin, hc->head.contlen, hc->head.transenc); + if(hin != nil){ + alarm(15*60*1000); + s = hreadbuf(hin, hin->pos); + alarm(0); + } + if(s == nil){ + hfail(hc, HBadReq, nil); + exits("failed"); + } + t = strchr(s, '\n'); + if(t != nil) + *t = '\0'; + }else{ + hunallowed(hc, "GET, HEAD, PUT"); + exits("unallowed"); + } + + if(s == nil){ + hfail(hc, HNoData, "wiki"); + exits("failed"); + } + + text = nil; + title = nil; + service = nil; + author = "???"; + comment = ""; + base = nil; + version = ~0; + nf = getfields(s, f, nelem(f), 1, "&"); + for(i=0; i<nf; i++){ + if((p = strchr(f[i], '=')) == nil) + continue; + *p++ = '\0'; + if(strcmp(f[i], "title")==0) + title = p; + else if(strcmp(f[i], "version")==0) + version = strtoul(unhttp(p), 0, 10); + else if(strcmp(f[i], "text")==0) + text = p; + else if(strcmp(f[i], "service")==0) + service = p; + else if(strcmp(f[i], "comment")==0) + comment = p; + else if(strcmp(f[i], "author")==0) + author = p; + else if(strcmp(f[i], "base")==0) + base = p; + } + + syslog(0, LOG, "%s post s %s t '%s' v %ld a %s c %s b %s t 0x%p", + hp->remotesys, service, title, (long)version, author, comment, base, text); + + title = unhttp(title); + comment = unhttp(comment); + service = unhttp(service); + text = unhttp(text); + author = unhttp(author); + base = unhttp(base); + + if(title==nil || version==~0 || text==nil || text[0]=='\0' || base == nil + || service == nil || strchr(title, '\n') || strchr(comment, '\n') + || dangerous(service) || strchr(service, '/') || strlen(service)>20){ + syslog(0, LOG, "%s failed dangerous", hp->remotesys); + hfail(hc, HSyntax); + exits("failed"); + } + + syslog(0, LOG, "%s post s %s t '%s' v %ld a %s c %s", + hp->remotesys, service, title, (long)version, author, comment); + + if(strlen(text) > MaxLog) + text[MaxLog] = '\0'; + + mountwiki(hc, service); + url = dowiki(hc, title, author, comment, base, version, text); + hredirected(hc, "303 See Other", url); + exits(nil); +} |