git: plan9front

Download patch

ref: 40afddb71da82ba9bde364ae0e632bf5d9b703d3
parent: 371fac82557c4fc17c70d8491d4e6bc5f37d2899
author: Jacob Moody <moody@posixcafe.org>
date: Sun Jun 1 02:04:44 EDT 2025

libhttpd: the reports of my death have been exaggerated

fucking venti

--- /dev/null
+++ b/sys/src/cmd/ip/httpd/anonymous.c
@@ -1,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("/");
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/authorize.c
@@ -1,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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/classify.c
@@ -1,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" },
+	{ "at",	"austria" },
+	{ "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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/content.c
@@ -1,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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/emem.c
@@ -1,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;
+}
+
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/hints.c
@@ -1,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);
+}
+
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/httpd.c
@@ -1,0 +1,608 @@
+#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)
+{
+	if(procsetuser("none") < 0)
+		sysfatal("can't become none: %r");
+	if(newns("none", nil) < 0)
+		sysfatal("can't build normal namespace: %r");
+	if(addns("none", namespace) < 0)
+		sysfatal("can't build httpd namespace: %r");
+}
+
+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;
+
+	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){
+				TLSconn conn;
+
+				memset(&conn, 0, sizeof(conn));
+				conn.cert = certificate;
+				conn.certlen = certlen;
+				if (certchain != nil)
+					conn.chain = certchain;
+				data = tlsServer(data, &conn);
+				free(conn.cert);
+				free(conn.sessionID);
+				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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/httpsrv.h
@@ -1,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;
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/imagemap.c
@@ -1,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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/init.c
@@ -1,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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/log.c
@@ -1,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 */
+	}
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/man2html.c
@@ -1,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);
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/mkfile
@@ -1,0 +1,76 @@
+</$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
+
+CLEANFILES=$LIB
+
+</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
+
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/netlib_find.c
@@ -1,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, "") == -1)
+		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, "&lt;");
+			w += 4;
+		}else if(*s == '>'){
+			strcpy(w, "&gt;");
+			w += 4;
+		}else if(*s == '&'){
+			strcpy(w, "&amp;");
+			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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/netlib_history.c
@@ -1,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 &lt; 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);
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/redirect.c
@@ -1,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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/save.c
@@ -1,0 +1,153 @@
+/*
+ * 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");
+	}
+
+	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);
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/sendfd.c
@@ -1,0 +1,466 @@
+#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", 1 + 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", 1 + r->stop - r->start);
+			hprint(hout, "\r\n");
+		}
+		hflush(hout);
+
+		if(seek(fd, r->start, 0) != r->start){
+			ok = -1;
+			break;
+		}
+		for(tr = 1 + r->stop - r->start; 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: %zud\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){
+			/*
+			 * for suffix, r->stop is a byte *length*
+			 * not the byte *offset* of last byte!
+			 */
+			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;
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/webls.c
@@ -1,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);
+}
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/webls.denied
@@ -1,0 +1,1 @@
+.*
--- /dev/null
+++ b/sys/src/cmd/ip/httpd/wikipost.c
@@ -1,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 == Runeerror)
+			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, "") == -1){
+		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);
+}
--- a/sys/src/cmd/ip/mkfile
+++ b/sys/src/cmd/ip/mkfile
@@ -30,7 +30,7 @@
 	sol\
 	wol\
 
-DIRS=ftpfs cifsd dhcpd ipconfig ppp snoopy
+DIRS=ftpfs cifsd dhcpd httpd ipconfig ppp snoopy
 
 BIN=/$objtype/bin/ip
 HFILES=dhcp.h arp.h glob.h icmp.h telnet.h
--- /dev/null
+++ b/sys/src/libhttpd/alloc.c
@@ -1,0 +1,35 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+/*
+ * memory allocators:
+ * h routines call canalloc; they should be used by everything else
+ * note this memory is wiped out at the start of each new request
+ * note: these routines probably shouldn't fatal.
+ */
+char*
+hstrdup(HConnect *c, char *s)
+{
+	char *t;
+	int n;
+
+	n = strlen(s) + 1;
+	t = binalloc(&c->bin, n, 0);
+	if(t == nil)
+		sysfatal("out of memory");
+	memmove(t, s, n);
+	return t;
+}
+
+void*
+halloc(HConnect *c, ulong n)
+{
+	void *p;
+
+	p = binalloc(&c->bin, n, 1);
+	if(p == nil)
+		sysfatal("out of memory");
+	return p;
+}
--- /dev/null
+++ b/sys/src/libhttpd/checkcontent.c
@@ -1,0 +1,33 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+int
+hcheckcontent(HContent *me, HContent *oks, char *list, int size)
+{
+	HContent *ok;
+
+	if(oks == nil || me == nil)
+		return 1;
+	for(ok = oks; ok != nil; ok = ok->next){
+		if((cistrcmp(ok->generic, me->generic) == 0 || strcmp(ok->generic, "*") == 0)
+		&& (me->specific == nil || cistrcmp(ok->specific, me->specific) == 0 || strcmp(ok->specific, "*") == 0)){
+			if(ok->mxb > 0 && size > ok->mxb)
+				return 0;
+			return 1;
+		}
+	}
+
+	USED(list);
+	if(0){
+		fprint(2, "list: %s/%s not found\n", me->generic, me->specific);
+		for(; oks != nil; oks = oks->next){
+			if(oks->specific)
+				fprint(2, "\t%s/%s\n", oks->generic, oks->specific);
+			else
+				fprint(2, "\t%s\n", oks->generic);
+		}
+	}
+	return 0;
+}
--- /dev/null
+++ b/sys/src/libhttpd/date.c
@@ -1,0 +1,215 @@
+#include <u.h>
+#include <libc.h>
+#include <httpd.h>
+
+/*
+ * print dates in the format
+ * Wkd, DD Mon YYYY HH:MM:SS GMT
+ * parse dates of formats
+ * Wkd, DD Mon YYYY HH:MM:SS GMT
+ * Weekday, DD-Mon-YY HH:MM:SS GMT
+ * Wkd Mon ( D|DD) HH:MM:SS YYYY
+ * plus anything similar
+ */
+static char *
+weekdayname[7] =
+{
+	"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
+};
+static char *
+wdayname[7] =
+{
+	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
+};
+
+static char *
+monname[12] =
+{
+	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
+};
+
+static	int	dateindex(char*, char**, int);
+
+static int
+dtolower(int c)
+{
+	if(c >= 'A' && c <= 'Z')
+		return c - 'A' + 'a';
+	return c;
+}
+
+static int
+disalpha(int c)
+{
+	return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z';
+}
+
+static int
+disdig(int c)
+{
+	return c >= '0' && c <= '9';
+}
+
+int
+hdatefmt(Fmt *f)
+{
+	Tm *tm;
+	ulong t;
+
+	t = va_arg(f->args, ulong);
+	tm = gmtime(t);
+	return fmtprint(f, "%s, %.2d %s %.4d %.2d:%.2d:%.2d GMT",
+		wdayname[tm->wday], tm->mday, monname[tm->mon], tm->year+1900,
+		tm->hour, tm->min, tm->sec);
+}
+
+static char*
+dateword(char *date, char *buf)
+{
+	char *p;
+	int c;
+
+	p = buf;
+	while(!disalpha(c = *date) && !disdig(c) && c)
+		date++;
+	while(disalpha(c = *date)){
+		if(p - buf < 30)
+			*p++ = dtolower(c);
+		date++;
+	}
+	*p = 0;
+	return date;
+}
+
+static int
+datenum(char **d)
+{
+	char *date;
+	int c, n;
+
+	date = *d;
+	while(!disdig(c = *date) && c)
+		date++;
+	if(c == 0){
+		*d = date;
+		return -1;
+	}
+	n = 0;
+	while(disdig(c = *date)){
+		n = n * 10 + c - '0';
+		date++;
+	}
+	*d = date;
+	return n;
+}
+
+/*
+ * parse a date and return the seconds since the epoch
+ * return 0 for a failure
+ */
+ulong
+hdate2sec(char *date)
+{
+	Tm tm;
+	char buf[32];
+
+	memset(&tm, 0, sizeof(tm));
+
+	/*
+	 * Weekday|Wday
+	 */
+	date = dateword(date, buf);
+	tm.wday = dateindex(buf, wdayname, 7);
+	if(tm.wday < 0)
+		tm.wday = dateindex(buf, weekdayname, 7);
+	if(tm.wday < 0)
+		return 0;
+
+	/*
+	 * check for the two major formats
+	 */
+	date = dateword(date, buf);
+	tm.mon = dateindex(buf, monname, 12);
+	if(tm.mon >= 0){
+		/*
+		 * MM
+		 */
+		tm.mday = datenum(&date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return 0;
+
+		/*
+		 * HH:MM:SS
+		 */
+		tm.hour = datenum(&date);
+		if(tm.hour < 0 || tm.hour >= 24)
+			return 0;
+		tm.min = datenum(&date);
+		if(tm.min < 0 || tm.min >= 60)
+			return 0;
+		tm.sec = datenum(&date);
+		if(tm.sec < 0 || tm.sec >= 60)
+			return 0;
+
+		/*
+		 * YYYY
+		 */
+		tm.year = datenum(&date);
+		if(tm.year < 70 || tm.year > 99 && tm.year < 1970)
+			return 0;
+		if(tm.year >= 1970)
+			tm.year -= 1900;
+	}else{
+		/*
+		 * MM-Mon-(YY|YYYY)
+		 */
+		tm.mday = datenum(&date);
+		if(tm.mday < 1 || tm.mday > 31)
+			return 0;
+		date = dateword(date, buf);
+		tm.mon = dateindex(buf, monname, 12);
+		if(tm.mon < 0 || tm.mon >= 12)
+			return 0;
+		tm.year = datenum(&date);
+		if(tm.year < 70 || tm.year > 99 && tm.year < 1970)
+			return 0;
+		if(tm.year >= 1970)
+			tm.year -= 1900;
+
+		/*
+		 * HH:MM:SS
+		 */
+		tm.hour = datenum(&date);
+		if(tm.hour < 0 || tm.hour >= 24)
+			return 0;
+		tm.min = datenum(&date);
+		if(tm.min < 0 || tm.min >= 60)
+			return 0;
+		tm.sec = datenum(&date);
+		if(tm.sec < 0 || tm.sec >= 60)
+			return 0;
+
+		/*
+		 * timezone
+		 */
+		dateword(date, buf);
+		if(strncmp(buf, "gmt", 3) != 0)
+			return 0;
+	}
+
+	strcpy(tm.zone, "GMT");
+	tm.tzoff = 0;
+	tm.yday = 0;
+	return tm2sec(&tm);
+}
+
+static int
+dateindex(char *d, char **tab, int n)
+{
+	int i;
+
+	for(i = 0; i < n; i++)
+		if(cistrcmp(d, tab[i]) == 0)
+			return i;
+	return -1;
+}
--- /dev/null
+++ b/sys/src/libhttpd/escape.h
@@ -1,0 +1,124 @@
+
+Htmlesc htmlesc[] =
+{
+	{ "&#161;",	L'¡', },
+	{ "&#162;",	L'¢', },
+	{ "&#163;",	L'£', },
+	{ "&#164;",	L'¤', },
+	{ "&#165;",	L'¥', },
+	{ "&#166;",	L'¦', },
+	{ "&#167;",	L'§', },
+	{ "&#168;",	L'¨', },
+	{ "&#169;",	L'©', },
+	{ "&#170;",	L'ª', },
+	{ "&#171;",	L'«', },
+	{ "&#172;",	L'¬', },
+	{ "&#173;",	L'­', },
+	{ "&#174;",	L'®', },
+	{ "&#175;",	L'¯', },
+	{ "&#176;",	L'°', },
+	{ "&#177;",	L'±', },
+	{ "&#178;",	L'²', },
+	{ "&#179;",	L'³', },
+	{ "&#180;",	L'´', },
+	{ "&#181;",	L'µ', },
+	{ "&#182;",	L'¶', },
+	{ "&#183;",	L'·', },
+	{ "&#184;",	L'¸', },
+	{ "&#185;",	L'¹', },
+	{ "&#186;",	L'º', },
+	{ "&#187;",	L'»', },
+	{ "&#188;",	L'¼', },
+	{ "&#189;",	L'½', },
+	{ "&#190;",	L'¾', },
+	{ "&#191;",	L'¿', },
+	{ "&Agrave;",	L'À', },
+	{ "&Aacute;",	L'Á', },
+	{ "&Acirc;",	L'Â', },
+	{ "&Atilde;",	L'Ã', },
+	{ "&Auml;",	L'Ä', },
+	{ "&Aring;",	L'Å', },
+	{ "&AElig;",	L'Æ', },
+	{ "&Ccedil;",	L'Ç', },
+	{ "&Egrave;",	L'È', },
+	{ "&Eacute;",	L'É', },
+	{ "&Ecirc;",	L'Ê', },
+	{ "&Euml;",	L'Ë', },
+	{ "&Igrave;",	L'Ì', },
+	{ "&Iacute;",	L'Í', },
+	{ "&Icirc;",	L'Î', },
+	{ "&Iuml;",	L'Ï', },
+	{ "&ETH;",	L'Ð', },
+	{ "&Ntilde;",	L'Ñ', },
+	{ "&Ograve;",	L'Ò', },
+	{ "&Oacute;",	L'Ó', },
+	{ "&Ocirc;",	L'Ô', },
+	{ "&Otilde;",	L'Õ', },
+	{ "&Ouml;",	L'Ö', },
+	{ "&215;",	L'×', },
+	{ "&Oslash;",	L'Ø', },
+	{ "&Ugrave;",	L'Ù', },
+	{ "&Uacute;",	L'Ú', },
+	{ "&Ucirc;",	L'Û', },
+	{ "&Uuml;",	L'Ü', },
+	{ "&Yacute;",	L'Ý', },
+	{ "&THORN;",	L'Þ', },
+	{ "&szlig;",	L'ß', },
+	{ "&agrave;",	L'à', },
+	{ "&aacute;",	L'á', },
+	{ "&acirc;",	L'â', },
+	{ "&atilde;",	L'ã', },
+	{ "&auml;",	L'ä', },
+	{ "&aring;",	L'å', },
+	{ "&aelig;",	L'æ', },
+	{ "&ccedil;",	L'ç', },
+	{ "&egrave;",	L'è', },
+	{ "&eacute;",	L'é', },
+	{ "&ecirc;",	L'ê', },
+	{ "&euml;",	L'ë', },
+	{ "&igrave;",	L'ì', },
+	{ "&iacute;",	L'í', },
+	{ "&icirc;",	L'î', },
+	{ "&iuml;",	L'ï', },
+	{ "&eth;",	L'ð', },
+	{ "&ntilde;",	L'ñ', },
+	{ "&ograve;",	L'ò', },
+	{ "&oacute;",	L'ó', },
+	{ "&ocirc;",	L'ô', },
+	{ "&otilde;",	L'õ', },
+	{ "&ouml;",	L'ö', },
+	{ "&247;",	L'÷', },
+	{ "&oslash;",	L'ø', },
+	{ "&ugrave;",	L'ù', },
+	{ "&uacute;",	L'ú', },
+	{ "&ucirc;",	L'û', },
+	{ "&uuml;",	L'ü', },
+	{ "&yacute;",	L'ý', },
+	{ "&thorn;",	L'þ', },
+	{ "&yuml;",	L'ÿ', },
+
+	{ "&quot;",	L'"', },
+	{ "&#39;",	L'\'', }, /* Note &apos; is valid XML but not valid HTML */
+	{ "&amp;",	L'&', },
+	{ "&lt;",	L'<', },
+	{ "&gt;",	L'>', },
+
+	{ "CAP-DELTA",	L'Δ', },
+	{ "ALPHA",	L'α', },
+	{ "BETA",	L'β', },
+	{ "DELTA",	L'δ', },
+	{ "EPSILON",	L'ε', },
+	{ "THETA",	L'θ', },
+	{ "MU",		L'μ', },
+	{ "PI",		L'π', },
+	{ "TAU",	L'τ', },
+	{ "CHI",	L'χ', },
+
+	{ "<-",		L'←', },
+	{ "^",		L'↑', },
+	{ "->",		L'→', },
+	{ "v",		L'↓', },
+	{ "!=",		L'≠', },
+	{ "<=",		L'≤', },
+	{ nil, 0 },
+};
--- /dev/null
+++ b/sys/src/libhttpd/fail.c
@@ -1,0 +1,93 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+typedef struct Error	Error;
+
+struct Error
+{
+	char	*num;
+	char	*concise;
+	char	*verbose;
+};
+
+Error errormsg[] =
+{
+	[HInternal]	{"500 Internal Error", "Internal Error",
+		"This server could not process your request due to an internal error."},
+	[HTempFail]	{"500 Internal Error", "Temporary Failure",
+		"The object %s is currently inaccessible.<p>Please try again later."},
+	[HUnimp]	{"501 Not implemented", "Command not implemented",
+		"This server does not implement the %s command."},
+	[HUnkVers]	{"501 Not Implemented", "Unknown http version",
+		"This server does not know how to respond to http version %s."},
+	[HBadCont]	{"501 Not Implemented", "Impossible format",
+		"This server cannot produce %s in any of the formats your client accepts."},
+	[HBadReq]	{"400 Bad Request", "Strange Request",
+		"Your client sent a query that this server could not understand."},
+	[HSyntax]	{"400 Bad Request", "Garbled Syntax",
+		"Your client sent a query with incoherent syntax."},
+	[HBadSearch]	{"400 Bad Request", "Inapplicable Search",
+		"Your client sent a search that cannot be applied to %s."},
+	[HNotFound]	{"404 Not Found", "Object not found",
+		"The object %s does not exist on this server."},
+	[HNoSearch]	{"403 Forbidden", "Search not supported",
+		"The object %s does not support the search command."},
+	[HNoData]	{"403 Forbidden", "No data supplied",
+		"Search or forms data must be supplied to %s."},
+	[HExpectFail]	{"403 Expectation Failed", "Expectation Failed",
+		"This server does not support some of your request's expectations."},
+	[HUnauth]	{"403 Forbidden", "Forbidden",
+		"You are not allowed to see the object %s."},
+	[HOK]		{"200 OK", "everything is fine"},
+};
+
+/*
+ * write a failure message to the net and exit
+ */
+int
+hfail(HConnect *c, int reason, ...)
+{
+	Hio *hout;
+	char makeup[HBufSize], err[ERRMAX];
+	va_list arg;
+	int n;
+
+	rerrstr(err, sizeof err);
+	hout = &c->hout;
+	va_start(arg, reason);
+	vseprint(makeup, makeup+HBufSize, errormsg[reason].verbose, arg);
+	va_end(arg);
+	/*
+	 * this additional information has proved useful when debugging
+	 * complex http configuration problems.
+	 */
+	n = snprint(c->xferbuf, HBufSize, "<head><title>%s</title></head>\n"
+		"<body><h1>%s</h1>\n%s<p>\n"
+		"errstr: %s<br>\n"
+		"uri host: %s<br>\n"
+		"header host: %s<br>\nactual host: %s\n</body>\n",
+		errormsg[reason].concise, errormsg[reason].concise, makeup,
+		err,
+		(c->req.urihost? c->req.urihost: ""),
+		c->head.host, hmydomain);
+
+	hprint(hout, "%s %s\r\n", hversion, errormsg[reason].num);
+	hprint(hout, "Date: %D\r\n", time(nil));
+	hprint(hout, "Server: Plan9\r\n");
+	hprint(hout, "Content-Type: text/html\r\n");
+	hprint(hout, "Content-Length: %d\r\n", 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(c->req.meth == nil || strcmp(c->req.meth, "HEAD") != 0)
+		hwrite(hout, c->xferbuf, n);
+
+	if(c->replog)
+		c->replog(c, "Reply: %s\nReason: %s\n", errormsg[reason].num, errormsg[reason].concise);
+	return hflush(hout);
+}
--- /dev/null
+++ b/sys/src/libhttpd/gethead.c
@@ -1,0 +1,40 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+/*
+ * read in some header lines, either one or all of them.
+ * copy results into header log buffer.
+ */
+int
+hgethead(HConnect *c, int many)
+{
+	Hio *hin;
+	char *s, *p, *pp;
+	int n;
+
+	hin = &c->hin;
+	for(;;){
+		s = (char*)hin->pos;
+		pp = s;
+		while(p = memchr(pp, '\n', (char*)hin->stop - pp)){
+			if(!many || p == pp || (p == pp + 1 && *pp == '\r')){
+				pp = p + 1;
+				break;
+			}
+			pp = p + 1;
+		}
+		hin->pos = (uchar*)pp;
+		n = pp - s;
+		if(c->hstop + n > &c->header[HBufSize])
+			return -1;
+		memmove(c->hstop, s, n);
+		c->hstop += n;
+		*c->hstop = '\0';
+		if(p != nil)
+			return 0;
+		if(hreadbuf(hin, hin->pos) == nil || hin->state == Hend)
+			return -1;
+	}
+}
--- /dev/null
+++ b/sys/src/libhttpd/hio.c
@@ -1,0 +1,488 @@
+#include <u.h>
+#include <libc.h>
+#include <httpd.h>
+
+static	char	hstates[] = "nrewE";
+static	char	hxfers[] = " x";
+static int _hflush(Hio*, int, int);
+
+int
+hinit(Hio *h, int fd, int mode)
+{
+	if(fd == -1 || mode != Hread && mode != Hwrite)
+		return -1;
+	h->hh = nil;
+	h->fd = fd;
+	h->seek = 0;
+	h->state = mode;
+	h->start = h->buf + 16;		/* leave space for chunk length */
+	h->stop = h->pos = h->start;
+	if(mode == Hread){
+		h->bodylen = ~0UL;
+		*h->pos = '\0';
+	}else
+		h->stop = h->start + Hsize;
+	return 0;
+}
+
+int
+hiserror(Hio *h)
+{
+	return h->state == Herr;
+}
+
+int
+hgetc(Hio *h)
+{
+	uchar *p;
+
+	p = h->pos;
+	if(p < h->stop){
+		h->pos = p + 1;
+		return *p;
+	}
+	p -= UTFmax;
+	if(p < h->start)
+		p = h->start;
+	if(!hreadbuf(h, p) || h->pos == h->stop)
+		return -1;
+	return *h->pos++;
+}
+
+int
+hungetc(Hio *h)
+{
+	if(h->state == Hend)
+		h->state = Hread;
+	else if(h->state == Hread)
+		h->pos--;
+	if(h->pos < h->start || h->state != Hread){
+		h->state = Herr;
+		h->pos = h->stop;
+		return -1;
+	}
+	return 0;
+}
+
+/*
+ * fill the buffer, saving contents from vsave onwards.
+ * nothing is saved if vsave is nil.
+ * returns the beginning of the buffer.
+ *
+ * understands message body sizes and chunked transfer encoding
+ */
+void *
+hreadbuf(Hio *h, void *vsave)
+{
+	Hio *hh;
+	uchar *save;
+	int c, in, cpy, dpos;
+
+	save = vsave;
+	if(save && (save < h->start || save > h->stop)
+	|| h->state != Hread && h->state != Hend){
+		h->state = Herr;
+		h->pos = h->stop;
+		return nil;
+	}
+
+	dpos = 0;
+	if(save && h->pos > save)
+		dpos = h->pos - save;
+	cpy = 0;
+	if(save){
+		cpy = h->stop - save;
+		memmove(h->start, save, cpy);
+	}
+	h->seek += h->stop - h->start - cpy;
+	h->pos = h->start + dpos;
+
+	in = Hsize - cpy;
+	if(h->state == Hend)
+		in = 0;
+	else if(in > h->bodylen)
+		in = h->bodylen;
+
+	/*
+	 * for chunked encoding, fill buffer,
+	 * then read in new chunk length and wipe out that line
+	 */
+	hh = h->hh;
+	if(hh != nil){
+		if(!in && h->xferenc && h->state != Hend){
+			if(h->xferenc == 2){
+				c = hgetc(hh);
+				if(c == '\r')
+					c = hgetc(hh);
+				if(c != '\n'){
+					h->pos = h->stop;
+					h->state = Herr;
+					return nil;
+				}
+			}
+			h->xferenc = 2;
+			in = 0;
+			while((c = hgetc(hh)) != '\n'){
+				if(c >= '0' && c <= '9')
+					c -= '0';
+				else if(c >= 'a' && c <= 'f')
+					c -= 'a' - 10;
+				else if(c >= 'A' && c <= 'F')
+					c -= 'A' - 10;
+				else
+					break;
+				in = in * 16 + c;
+			}
+			while(c != '\n'){
+				if(c < 0){
+					h->pos = h->stop;
+					h->state = Herr;
+					return nil;
+				}
+				c = hgetc(hh);
+			}
+			h->bodylen = in;
+
+			in = Hsize - cpy;
+			if(in > h->bodylen)
+				in = h->bodylen;
+		}
+		if(in){
+			while(hh->pos + in > hh->stop){
+				if(hreadbuf(hh, hh->pos) == nil){
+					h->pos = h->stop;
+					h->state = Herr;
+					return nil;
+				}
+			}
+			memmove(h->start + cpy, hh->pos, in);
+			hh->pos += in;
+		}
+	}else if(in){
+		if((in = read(h->fd, h->start + cpy, in)) < 0){
+			h->state = Herr;
+			h->pos = h->stop;
+			return nil;
+		}
+	}
+	if(in == 0)
+		h->state = Hend;
+
+	h->bodylen -= in;
+
+	h->stop = h->start + cpy + in;
+	*h->stop = '\0';
+	if(h->pos == h->stop)
+		return nil;
+	return h->start;
+}
+
+int
+hbuflen(Hio *h, void *p)
+{
+	return h->stop - (uchar*)p;
+}
+
+/*
+ * prepare to receive a message body
+ * len is the content length (~0 => unspecified)
+ * te is the transfer encoding
+ * returns < 0 if setup failed
+ */
+Hio*
+hbodypush(Hio *hh, ulong len, HFields *te)
+{
+	Hio *h;
+	int xe;
+
+	if(hh->state != Hread)
+		return nil;
+	xe = 0;
+	if(te != nil){
+		if(te->params != nil || te->next != nil)
+			return nil;
+		if(cistrcmp(te->s, "chunked") == 0){
+			xe = 1;
+			len = 0;
+		}else if(cistrcmp(te->s, "identity") == 0){
+			;
+		}else
+			return nil;
+	}
+
+	h = malloc(sizeof *h);
+	if(h == nil)
+		return nil;
+
+	h->hh = hh;
+	h->fd = -1;
+	h->seek = 0;
+	h->state = Hread;
+	h->xferenc = xe;
+	h->start = h->buf + 16;		/* leave space for chunk length */
+	h->stop = h->pos = h->start;
+	*h->pos = '\0';
+	h->bodylen = len;
+	return h;
+}
+
+/*
+ * dump the state of the io buffer into a string
+ */
+char *
+hunload(Hio *h)
+{
+	uchar *p, *t, *stop, *buf;
+	int ne, n, c;
+
+	stop = h->stop;
+	ne = 0;
+	for(p = h->pos; p < stop; p++){
+		c = *p;
+		if(c == 0x80)
+			ne++;
+	}
+	p = h->pos;
+
+	n = (stop - p) + ne + 3;
+	buf = mallocz(n, 1);
+	if(buf == nil)
+		return nil;
+	buf[0] = hstates[h->state];
+	buf[1] = hxfers[h->xferenc];
+
+	t = &buf[2];
+	for(; p < stop; p++){
+		c = *p;
+		if(c == 0 || c == 0x80){
+			*t++ = 0x80;
+			if(c == 0x80)
+				*t++ = 0x80;
+		}else
+			*t++ = c;
+	}
+	*t++ = '\0';
+	if(t != buf + n)
+		return nil;
+	return (char*)buf;
+}
+
+/*
+ * read the io buffer state from a string
+ */
+int
+hload(Hio *h, char *buf)
+{
+	uchar *p, *t, *stop;
+	char *s;
+	int c;
+
+	s = strchr(hstates, buf[0]);
+	if(s == nil)
+		return -1;
+	h->state = s - hstates;
+
+	s = strchr(hxfers, buf[1]);
+	if(s == nil)
+		return -1;
+	h->xferenc = s - hxfers;
+
+	t = h->start;
+	stop = t + Hsize;
+	for(p = (uchar*)&buf[2]; c = *p; p++){
+		if(c == 0x80){
+			if(p[1] != 0x80)
+				c = 0;
+			else
+				p++;
+		}
+		*t++ = c;
+		if(t >= stop)
+			return -1;
+	}
+	*t = '\0';
+	h->pos = h->start;
+	h->stop = t;
+	h->seek = 0;
+	return 0;
+}
+
+void
+hclose(Hio *h)
+{
+	if(h->fd >= 0){
+		if(h->state == Hwrite)
+			hxferenc(h, 0);
+		close(h->fd);
+	}
+	h->stop = h->pos = nil;
+	h->fd = -1;
+}
+
+/*
+ * flush the buffer and possibly change encoding modes
+ */
+int
+hxferenc(Hio *h, int on)
+{
+	if(h->xferenc && !on && h->pos != h->start)
+		hflush(h);
+	if(_hflush(h, 1, 0) < 0)
+		return -1;
+	h->xferenc = !!on;
+	return 0;
+}
+
+int
+hputc(Hio *h, int c)
+{
+	uchar *p;
+
+	p = h->pos;
+	if(p < h->stop){
+		h->pos = p + 1;
+		return *p = c;
+	}
+	if(hflush(h) < 0)
+		return -1;
+	return *h->pos++ = c;
+}
+
+static int
+fmthflush(Fmt *f)
+{
+	Hio *h;
+
+	h = f->farg;
+	h->pos = f->to;
+	if(hflush(h) < 0)
+		return 0;
+	f->stop = h->stop;
+	f->to = h->pos;
+	f->start = h->pos;
+	return 1;
+}
+
+int
+hvprint(Hio *h, char *fmt, va_list args)
+{
+	int n;
+	Fmt f;
+
+	f.runes = 0;
+	f.stop = h->stop;
+	f.to = h->pos;
+	f.start = h->pos;
+	f.flush = fmthflush;
+	f.farg = h;
+	f.nfmt = 0;
+//	fmtlocaleinit(&f, nil, nil, nil);
+	n = fmtvprint(&f, fmt, args);
+	h->pos = f.to;
+	return n;
+}
+
+int
+hprint(Hio *h, char *fmt, ...)
+{
+	int n;
+	va_list arg;
+
+	va_start(arg, fmt);
+	n = hvprint(h, fmt, arg);
+	va_end(arg);
+	return n;
+}
+
+static int
+_hflush(Hio *h, int force, int dolength)
+{
+	uchar *s;
+	int w;
+
+	if(h == nil)
+		return -1;
+	if(h->state != Hwrite){
+		h->state = Herr;
+		h->stop = h->pos;
+		return -1;
+	}
+	s = h->start;
+	w = h->pos - s;
+	if(w == 0 && !force)
+		return 0;
+	if(h->xferenc){
+		*--s = '\n';
+		*--s = '\r';
+		do{
+			*--s = "0123456789abcdef"[w & 0xf];
+			w >>= 4;
+		}while(w);
+		h->pos[0] = '\r';
+		h->pos[1] = '\n';
+		w = &h->pos[2] - s;
+	}
+	if(dolength)
+		fprint(h->fd, "Content-Length: %d\r\n\r\n", w);
+	if(write(h->fd, s, w) != w){
+		h->state = Herr;
+		h->stop = h->pos;
+		return -1;
+	}
+	h->seek += w;
+	h->pos = h->start;
+	return 0;
+}
+
+int
+hflush(Hio *h)
+{
+	return _hflush(h, 0, 0);
+}
+
+int
+hlflush(Hio* h)
+{
+	return _hflush(h, 0, 1);
+}
+
+int
+hwrite(Hio *h, void *vbuf, int len)
+{
+	uchar *buf;
+	int n, m;
+
+	buf = vbuf;
+	n = len;
+	if(n < 0 || h->state != Hwrite){
+		h->state = Herr;
+		h->stop = h->pos;
+		return -1;
+	}
+	if(h->pos + n >= h->stop){
+		if(h->start != h->pos)
+			if(hflush(h) < 0)
+				return -1;
+		while(h->pos + n >= h->stop){
+			m = h->stop - h->pos;
+			if(h->xferenc){
+				memmove(h->pos, buf, m);
+				h->pos += m;
+				if(hflush(h) < 0)
+					return -1;
+			}else{
+				if(write(h->fd, buf, m) != m){
+					h->state = Herr;
+					h->stop = h->pos;
+					return -1;
+				}
+				h->seek += m;
+			}
+			n -= m;
+			buf += m;
+		}
+	}
+	memmove(h->pos, buf, n);
+	h->pos += n;
+	return len;
+}
--- /dev/null
+++ b/sys/src/libhttpd/httpfmt.c
@@ -1,0 +1,30 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+int
+httpfmt(Fmt *f)
+{
+	char buf[HMaxWord*2];
+	Rune r;
+	char *t, *s;
+	Htmlesc *l;
+
+	s = va_arg(f->args, char*);
+	for(t = buf; t < buf + sizeof(buf) - 8; ){
+		s += chartorune(&r, s);
+		if(r == 0)
+			break;
+		for(l = htmlesc; l->name != nil; l++)
+			if(l->value == r)
+				break;
+		if(l->name != nil){
+			strcpy(t, l->name);
+			t += strlen(t);
+		}else
+			*t++ = r;
+	}
+	*t = 0;
+	return fmtstrcpy(f, buf);
+}
--- /dev/null
+++ b/sys/src/libhttpd/httpunesc.c
@@ -1,0 +1,49 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+/*
+ *  go from http with latin1 escapes to utf,
+ *  we assume that anything >= Runeself is already in utf
+ */
+char *
+httpunesc(HConnect *cc, char *s)
+{
+	char *t, *v;
+	int c;
+	Htmlesc *e;
+
+	v = halloc(cc, UTFmax*strlen(s) + 1);
+	for(t = v; c = *s;){
+		if(c == '&'){
+			if(s[1] == '#' && s[2] && s[3] && s[4] && s[5] == ';'){
+				c = strtol(s+2, 0, 10);
+				if(c < Runeself){
+					*t++ = c;
+					s += 6;
+					continue;
+				}
+				if(c < 256 && c >= 161){
+					e = &htmlesc[c-161];
+					t += runetochar(t, &e->value);
+					s += 6;
+					continue;
+				}
+			} else {
+				for(e = htmlesc; e->name != nil; e++)
+					if(strncmp(e->name, s, strlen(e->name)) == 0)
+						break;
+				if(e->name != nil){
+					t += runetochar(t, &e->value);
+					s += strlen(e->name);
+					continue;
+				}
+			}
+		}
+		*t++ = c;
+		s++;
+	}
+	*t = 0;
+	return v;
+}
--- /dev/null
+++ b/sys/src/libhttpd/lower.c
@@ -1,0 +1,19 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+char*
+hlower(char *p)
+{
+	char c;
+	char *x;
+
+	if(p == nil)
+		return p;
+
+	for(x = p; c = *x; x++)
+		if(c >= 'A' && c <= 'Z')
+			*x -= 'A' - 'a';
+	return p;
+}
--- /dev/null
+++ b/sys/src/libhttpd/mkfile
@@ -1,0 +1,27 @@
+</$objtype/mkfile
+
+LIB=/$objtype/lib/libhttpd.a
+OFILES=\
+	alloc.$O\
+	checkcontent.$O\
+	date.$O\
+	fail.$O\
+	gethead.$O\
+	hio.$O\
+	httpfmt.$O\
+	httpunesc.$O\
+	lower.$O\
+	okheaders.$O\
+	parse.$O\
+	parsereq.$O\
+	query.$O\
+	redirected.$O\
+	unallowed.$O\
+	urlfmt.$O\
+	urlunesc.$O\
+
+HFILES=\
+	/sys/include/httpd.h\
+	escape.h\
+
+</sys/src/cmd/mksyslib
--- /dev/null
+++ b/sys/src/libhttpd/okheaders.c
@@ -1,0 +1,22 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+/*
+ * write initial part of successful header
+ */
+void
+hokheaders(HConnect *c)
+{
+	Hio *hout;
+
+	hout = &c->hout;
+	hprint(hout, "%s 200 OK\r\n", hversion);
+	hprint(hout, "Server: Plan9\r\n");
+	hprint(hout, "Date: %D\r\n", time(nil));
+	if(c->head.closeit)
+		hprint(hout, "Connection: close\r\n");
+	else if(!http11(c))
+		hprint(hout, "Connection: Keep-Alive\r\n");
+}
--- /dev/null
+++ b/sys/src/libhttpd/parse.c
@@ -1,0 +1,1126 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <libsec.h>
+#include <bin.h>
+#include <httpd.h>
+#include "escape.h"
+
+typedef struct Hlex	Hlex;
+typedef struct MimeHead	MimeHead;
+
+enum
+{
+	/*
+	 * tokens
+	 */
+	Word	= 1,
+	QString,
+};
+
+#define UlongMax	4294967295UL
+
+struct Hlex
+{
+	int	tok;
+	int	eoh;
+	int	eol;			/* end of header line encountered? */
+	uchar	*hstart;		/* start of header */
+	jmp_buf	jmp;			/* jmp here to parse header */
+	char	wordval[HMaxWord];
+	HConnect *c;
+};
+
+struct MimeHead
+{
+	char	*name;
+	void	(*parse)(Hlex*, char*);
+	uchar	seen;
+	uchar	ignore;
+};
+
+static void	mimeaccept(Hlex*, char*);
+static void	mimeacceptchar(Hlex*, char*);
+static void	mimeacceptenc(Hlex*, char*);
+static void	mimeacceptlang(Hlex*, char*);
+static void	mimeagent(Hlex*, char*);
+static void	mimeauthorization(Hlex*, char*);
+static void	mimeconnection(Hlex*, char*);
+static void	mimecontlen(Hlex*, char*);
+static void	mimecookie(Hlex*, char*);
+static void	mimeexpect(Hlex*, char*);
+static void	mimefresh(Hlex*, char*);
+static void	mimefrom(Hlex*, char*);
+static void	mimehost(Hlex*, char*);
+static void	mimeifrange(Hlex*, char*);
+static void	mimeignore(Hlex*, char*);
+static void	mimematch(Hlex*, char*);
+static void	mimemodified(Hlex*, char*);
+static void	mimenomatch(Hlex*, char*);
+static void	mimerange(Hlex*, char*);
+static void	mimetransenc(Hlex*, char*);
+static void	mimeunmodified(Hlex*, char*);
+
+/*
+ * headers seen also include
+ * allow  cache-control chargeto
+ * content-encoding content-language content-location content-md5 content-range content-type
+ * date etag expires forwarded last-modified max-forwards pragma
+ * proxy-agent proxy-authorization proxy-connection
+ * ua-color ua-cpu ua-os ua-pixels
+ * upgrade via x-afs-tokens x-serial-number
+ */
+static MimeHead	mimehead[] =
+{
+	{"accept",		mimeaccept},
+	{"accept-charset",	mimeacceptchar},
+	{"accept-encoding",	mimeacceptenc},
+	{"accept-language",	mimeacceptlang},
+	{"authorization",	mimeauthorization},
+	{"connection",		mimeconnection},
+	{"content-length",	mimecontlen},
+	{"cookie",		mimecookie},
+	{"expect",		mimeexpect},
+	{"fresh",		mimefresh},
+	{"from",		mimefrom},
+	{"host",		mimehost},
+	{"if-match",		mimematch},
+	{"if-modified-since",	mimemodified},
+	{"if-none-match",	mimenomatch},
+	{"if-range",		mimeifrange},
+	{"if-unmodified-since",	mimeunmodified},
+	{"range",		mimerange},
+	{"transfer-encoding",	mimetransenc},
+	{"user-agent",		mimeagent},
+};
+
+char*		hmydomain;
+char*		hversion = "HTTP/1.1";
+
+static	void	lexhead(Hlex*);
+static	void	parsejump(Hlex*, char*);
+static	int	getc(Hlex*);
+static	void	ungetc(Hlex*);
+static	int	wordcr(Hlex*);
+static	int	wordnl(Hlex*);
+static	void	word(Hlex*, char*);
+static	int	lex1(Hlex*, int);
+static	int	lex(Hlex*);
+static	int	lexbase64(Hlex*);
+static	ulong	digtoul(char *s, char **e);
+
+/*
+ * flush and clean up junk from a request
+ */
+void
+hreqcleanup(HConnect *c)
+{
+	int i;
+
+	hxferenc(&c->hout, 0);
+	memset(&c->req, 0, sizeof(c->req));
+	memset(&c->head, 0, sizeof(c->head));
+	c->hpos = c->header;
+	c->hstop = c->header;
+	binfree(&c->bin);
+	for(i = 0; i < nelem(mimehead); i++){
+		mimehead[i].seen = 0;
+		mimehead[i].ignore = 0;
+	}
+}
+
+/*
+ * list of tokens
+ * if the client is HTTP/1.0,
+ * ignore headers which match one of the tokens.
+ * restarts parsing if necessary.
+ */
+static void
+mimeconnection(Hlex *h, char *)
+{
+	char *u, *p;
+	int reparse, i;
+
+	reparse = 0;
+	for(;;){
+		while(lex(h) != Word)
+			if(h->tok != ',')
+				goto breakout;
+
+		if(cistrcmp(h->wordval, "keep-alive") == 0)
+			h->c->head.persist = 1;
+		else if(cistrcmp(h->wordval, "close") == 0)
+			h->c->head.closeit = 1;
+		else if(!http11(h->c)){
+			for(i = 0; i < nelem(mimehead); i++){
+				if(cistrcmp(mimehead[i].name, h->wordval) == 0){
+					reparse = mimehead[i].seen && !mimehead[i].ignore;
+					mimehead[i].ignore = 1;
+					if(cistrcmp(mimehead[i].name, "authorization") == 0){
+						h->c->head.authuser = nil;
+						h->c->head.authpass = nil;
+					}
+				}
+			}
+		}
+
+		if(lex(h) != ',')
+			break;
+	}
+
+breakout:;
+	/*
+	 * if need to ignore headers we've already parsed,
+	 * reset & start over.  need to save authorization
+	 * info because it's written over when parsed.
+	 */
+	if(reparse){
+		u = h->c->head.authuser;
+		p = h->c->head.authpass;
+		memset(&h->c->head, 0, sizeof(h->c->head));
+		h->c->head.authuser = u;
+		h->c->head.authpass = p;
+
+		h->c->hpos = h->hstart;
+		longjmp(h->jmp, 1);
+	}
+}
+
+int
+hparseheaders(HConnect *c, int timeout)
+{
+	Hlex h;
+
+	c->head.fresh_thresh = 0;
+	c->head.fresh_have = 0;
+	c->head.persist = 0;
+	if(c->req.vermaj == 0){
+		c->head.host = hmydomain;
+		return 1;
+	}
+
+	memset(&h, 0, sizeof(h));
+	h.c = c;
+	if(timeout)
+		alarm(timeout);
+	if(hgethead(c, 1) < 0)
+		return -1;
+	if(timeout)
+		alarm(0);
+	h.hstart = c->hpos;
+
+	if(setjmp(h.jmp) == -1)
+		return -1;
+
+	h.eol = 0;
+	h.eoh = 0;
+	h.tok = '\n';
+	while(lex(&h) != '\n'){
+		if(h.tok == Word && lex(&h) == ':')
+			parsejump(&h, hstrdup(c, h.wordval));
+		while(h.tok != '\n')
+			lex(&h);
+		h.eol = h.eoh;
+	}
+
+	if(http11(c)){
+		/*
+		 * according to the http/1.1 spec,
+		 * these rules must be followed
+		 */
+		if(c->head.host == nil){
+			hfail(c, HBadReq, nil);
+			return -1;
+		}
+		if(c->req.urihost != nil)
+			c->head.host = c->req.urihost;
+		/*
+		 * also need to check host is actually this one
+		 */
+	}else if(c->head.host == nil)
+		c->head.host = hmydomain;
+	return 1;
+}
+
+/*
+ * mimeparams	: | mimeparams ";" mimepara
+ * mimeparam	: token "=" token | token "=" qstring
+ */
+static HSPairs*
+mimeparams(Hlex *h)
+{
+	HSPairs *p;
+	char *s;
+
+	p = nil;
+	for(;;){
+		if(lex(h) != Word)
+			break;
+		s = hstrdup(h->c, h->wordval);
+		if(lex(h) != Word && h->tok != QString)
+			break;
+		p = hmkspairs(h->c, s, hstrdup(h->c, h->wordval), p);
+	}
+	return hrevspairs(p);
+}
+
+/*
+ * mimehfields	: mimehfield | mimehfields commas mimehfield
+ * mimehfield	: token mimeparams
+ * commas	: "," | commas ","
+ */
+static HFields*
+mimehfields(Hlex *h)
+{
+	HFields *f;
+
+	f = nil;
+	for(;;){
+		while(lex(h) != Word)
+			if(h->tok != ',')
+				goto breakout;
+
+		f = hmkhfields(h->c, hstrdup(h->c, h->wordval), nil, f);
+
+		if(lex(h) == ';')
+			f->params = mimeparams(h);
+		if(h->tok != ',')
+			break;
+	}
+breakout:;
+	return hrevhfields(f);
+}
+
+/*
+ * parse a list of acceptable types, encodings, languages, etc.
+ */
+static HContent*
+mimeok(Hlex *h, char *name, int multipart, HContent *head)
+{
+	char *generic, *specific, *s;
+	float v;
+
+	/*
+	 * each type is separated by one or more commas
+	 */
+	while(lex(h) != Word)
+		if(h->tok != ',')
+			return head;
+
+	generic = hstrdup(h->c, h->wordval);
+	lex(h);
+	if(h->tok == '/' || multipart){
+		/*
+		 * at one time, IE5 improperly said '*' for single types
+		 */
+		if(h->tok != '/')
+			return nil;
+		if(lex(h) != Word)
+			return head;
+		specific = hstrdup(h->c, h->wordval);
+		if(!multipart && strcmp(specific, "*") != 0)
+			return head;
+		lex(h);
+	}else
+		specific = nil;
+	head = hmkcontent(h->c, generic, specific, head);
+
+	for(;;){
+		switch(h->tok){
+		case ';':
+			/*
+			 * should make a list of these params
+			 * for accept, they fall into two classes:
+			 *	up to a q=..., they modify the media type.
+			 *	afterwards, they acceptance criteria
+			 */
+			if(lex(h) == Word){
+				s = hstrdup(h->c, h->wordval);
+				if(lex(h) != '=' || lex(h) != Word && h->tok != QString)
+					return head;
+				v = strtod(h->wordval, nil);
+				if(strcmp(s, "q") == 0)
+					head->q = v;
+				else if(strcmp(s, "mxb") == 0)
+					head->mxb = v;
+				else{
+					/* cope with accept: application/xhtml+xml; profile=http://www.wapforum.org/xhtml, */
+					while(lex(h) == Word || (h->tok != ',' && h->eol == 0) )
+						;
+					return mimeok(h, name, multipart, head);
+				}
+			}
+			break;
+		case ',':
+			return  mimeok(h, name, multipart, head);
+		default:
+			return head;
+		}
+		lex(h);
+	}
+}
+
+/*
+ * parse a list of entity tags
+ * 1#entity-tag
+ * entity-tag = [weak] opaque-tag
+ * weak = "W/"
+ * opaque-tag = quoted-string
+ */
+static HETag*
+mimeetag(Hlex *h, HETag *head)
+{
+	HETag *e;
+	int weak;
+
+	for(;;){
+		while(lex(h) != Word && h->tok != QString)
+			if(h->tok != ',')
+				return head;
+
+		weak = 0;
+		if(h->tok == Word && strcmp(h->wordval, "*") != 0){
+			if(strcmp(h->wordval, "W") != 0)
+				return head;
+			if(lex(h) != '/' || lex(h) != QString)
+				return head;
+			weak = 1;
+		}
+
+		e = halloc(h->c, sizeof(HETag));
+		e->etag = hstrdup(h->c, h->wordval);
+		e->weak = weak;
+		e->next = head;
+		head = e;
+
+		if(lex(h) != ',')
+			return head;
+	}
+}
+
+/*
+ * ranges-specifier = byte-ranges-specifier
+ * byte-ranges-specifier = "bytes" "=" byte-range-set
+ * byte-range-set = 1#(byte-range-spec|suffix-byte-range-spec)
+ * byte-range-spec = byte-pos "-" [byte-pos]
+ * byte-pos = 1*DIGIT
+ * suffix-byte-range-spec = "-" suffix-length
+ * suffix-length = 1*DIGIT
+ *
+ * syntactically invalid range specifiers cause the
+ * entire header field to be ignored.
+ * it is syntactically incorrect for the second byte pos
+ * to be smaller than the first byte pos
+ */
+static HRange*
+mimeranges(Hlex *h, HRange *head)
+{
+	HRange *r, *rh, *tail;
+	char *w;
+	ulong start, stop;
+	int suf;
+
+	if(lex(h) != Word || strcmp(h->wordval, "bytes") != 0 || lex(h) != '=')
+		return head;
+
+	rh = nil;
+	tail = nil;
+	for(;;){
+		while(lex(h) != Word){
+			if(h->tok != ','){
+				if(h->tok == '\n')
+					goto breakout;
+				return head;
+			}
+		}
+
+		w = h->wordval;
+		start = 0;
+		suf = 1;
+		if(w[0] != '-'){
+			suf = 0;
+			start = digtoul(w, &w);
+			if(w[0] != '-')
+				return head;
+		}
+		w++;
+		stop = ~0UL;
+		if(w[0] != '\0'){
+			stop = digtoul(w, &w);
+			if(w[0] != '\0')
+				return head;
+			if(!suf && stop < start)
+				return head;
+		}
+
+		r = halloc(h->c, sizeof(HRange));
+		r->suffix = suf;
+		r->start = start;
+		r->stop = stop;
+		r->next = nil;
+		if(rh == nil)
+			rh = r;
+		else
+			tail->next = r;
+		tail = r;
+
+		if(lex(h) != ','){
+			if(h->tok == '\n')
+				break;
+			return head;
+		}
+	}
+breakout:;
+
+	if(head == nil)
+		return rh;
+
+	for(tail = head; tail->next != nil; tail = tail->next)
+		;
+	tail->next = rh;
+	return head;
+}
+
+static void
+mimeaccept(Hlex *h, char *name)
+{
+	h->c->head.oktype = mimeok(h, name, 1, h->c->head.oktype);
+}
+
+static void
+mimeacceptchar(Hlex *h, char *name)
+{
+	h->c->head.okchar = mimeok(h, name, 0, h->c->head.okchar);
+}
+
+static void
+mimeacceptenc(Hlex *h, char *name)
+{
+	h->c->head.okencode = mimeok(h, name, 0, h->c->head.okencode);
+}
+
+static void
+mimeacceptlang(Hlex *h, char *name)
+{
+	h->c->head.oklang = mimeok(h, name, 0, h->c->head.oklang);
+}
+
+static void
+mimemodified(Hlex *h, char *)
+{
+	lexhead(h);
+	h->c->head.ifmodsince = hdate2sec(h->wordval);
+}
+
+static void
+mimeunmodified(Hlex *h, char *)
+{
+	lexhead(h);
+	h->c->head.ifunmodsince = hdate2sec(h->wordval);
+}
+
+static void
+mimematch(Hlex *h, char *)
+{
+	h->c->head.ifmatch = mimeetag(h, h->c->head.ifmatch);
+}
+
+static void
+mimenomatch(Hlex *h, char *)
+{
+	h->c->head.ifnomatch = mimeetag(h, h->c->head.ifnomatch);
+}
+
+/*
+ * argument is either etag or date
+ */
+static void
+mimeifrange(Hlex *h, char *)
+{
+	int c, d, et;
+
+	et = 0;
+	c = getc(h);
+	while(c == ' ' || c == '\t')
+		c = getc(h);
+	if(c == '"')
+		et = 1;
+	else if(c == 'W'){
+		d = getc(h);
+		if(d == '/')
+			et = 1;
+		ungetc(h);
+	}
+	ungetc(h);
+	if(et){
+		h->c->head.ifrangeetag = mimeetag(h, h->c->head.ifrangeetag);
+	}else{
+		lexhead(h);
+		h->c->head.ifrangedate = hdate2sec(h->wordval);
+	}
+}
+
+static void
+mimerange(Hlex *h, char *)
+{
+	h->c->head.range = mimeranges(h, h->c->head.range);
+}
+
+/*
+ * parse it like cookies
+ */
+static void
+authdigest(Hlex *h, char *)
+{
+	char *s;
+	HSPairs *p;
+
+	p = nil;
+	for(;;){
+		while(lex(h) != Word)
+			if(h->tok != ';' && h->tok != ',')
+				goto breakout;
+		s = hstrdup(h->c, h->wordval);
+		while (lex(h) != Word && h->tok != QString)
+			if (h->tok != '=')
+				goto breakout;
+		p = hmkspairs(h->c, s, hstrdup(h->c, h->wordval), p);
+	}
+breakout:
+	h->c->head.authinfo = hrevspairs(p);
+}
+
+/*
+ * note: netscape and ie through versions 4.7 and 4
+ * support only basic authorization, so that is all that is supported here
+ *
+ * "Authorization" ":" "Basic" base64-user-pass
+ * where base64-user-pass is the base64 encoding of
+ * username ":" password
+ */
+static void
+authbasic(Hlex *h, char *)
+{
+	char *up, *p;
+	int n;
+
+	n = lexbase64(h);
+	if(!n)
+		return;
+
+	/*
+	 * wipe out source for password, so it won't be logged.
+	 * it is replaced by a single =,
+	 * which is valid base64, but not ok for an auth reponse.
+	 * therefore future parses of the header field will not overwrite
+	 * authuser and authpass.
+	 */
+	memmove(h->c->hpos - (n - 1), h->c->hpos, h->c->hstop - h->c->hpos);
+	h->c->hstop -= n - 1;
+	*h->c->hstop = '\0';
+	h->c->hpos -= n - 1;
+	h->c->hpos[-1] = '=';
+
+	up = halloc(h->c, n + 1);
+	n = dec64((uchar*)up, n, h->wordval, n);
+	up[n] = '\0';
+	p = strchr(up, ':');
+	if(p != nil){
+		*p++ = '\0';
+		h->c->head.authuser = hstrdup(h->c, up);
+		h->c->head.authpass = hstrdup(h->c, p);
+	}
+}
+
+/*
+ * "Authorization" ":" "Basic" | "Digest" ...
+ */
+static void
+mimeauthorization(Hlex *h, char *)
+{
+	int i;
+	static MimeHead authparser[] = {
+		{ "basic", authbasic },
+		{ "digest", authdigest },
+	};
+
+	if(lex(h) != Word)
+		return;
+
+	for (i = 0; i < nelem(authparser); i++)
+		if (cistrcmp(h->wordval, authparser[i].name) == 0) {
+			(*authparser[i].parse)(h, nil);
+			break;
+		}
+}
+
+static void
+mimeagent(Hlex *h, char *)
+{
+	lexhead(h);
+	h->c->head.client = hstrdup(h->c, h->wordval);
+}
+
+static void
+mimefrom(Hlex *h, char *)
+{
+	lexhead(h);
+}
+
+static void
+mimehost(Hlex *h, char *)
+{
+	char *hd;
+
+	lexhead(h);
+	for(hd = h->wordval; *hd == ' ' || *hd == '\t'; hd++)
+		;
+	h->c->head.host = hlower(hstrdup(h->c, hd));
+}
+
+/*
+ * if present, implies that a message body follows the headers
+ * "content-length" ":" digits
+ */
+static void
+mimecontlen(Hlex *h, char *)
+{
+	char *e;
+	ulong v;
+
+	if(lex(h) != Word)
+		return;
+	e = h->wordval;
+	v = digtoul(e, &e);
+	if(v == ~0UL || *e != '\0')
+		return;
+	h->c->head.contlen = v;
+}
+
+/*
+ * mimexpect	: "expect" ":" expects
+ * expects	: | expects "," expect
+ * expect	: "100-continue" | token | token "=" token expectparams | token "=" qstring expectparams
+ * expectparams	: ";" token | ";" token "=" token | token "=" qstring
+ * for now, we merely parse "100-continue" or anything else.
+ */
+static void
+mimeexpect(Hlex *h, char *)
+{
+	if(lex(h) != Word || cistrcmp(h->wordval, "100-continue") != 0 || lex(h) != '\n')
+		h->c->head.expectother = 1;
+	h->c->head.expectcont = 1;
+}
+
+static void
+mimetransenc(Hlex *h, char *)
+{
+	h->c->head.transenc = mimehfields(h);
+}
+
+static void
+mimecookie(Hlex *h, char *)
+{
+	char *s;
+	HSPairs *p;
+
+	p = nil;
+	for(;;){
+		while(lex(h) != Word)
+			if(h->tok != ';' && h->tok != ',')
+				goto breakout;
+		s = hstrdup(h->c, h->wordval);
+		while (lex(h) != Word && h->tok != QString)
+			if (h->tok != '=')
+				goto breakout;
+		p = hmkspairs(h->c, s, hstrdup(h->c, h->wordval), p);
+	}
+breakout:
+	h->c->head.cookie = hrevspairs(p);
+}
+
+static void
+mimefresh(Hlex *h, char *)
+{
+	char *s;
+
+	lexhead(h);
+	for(s = h->wordval; *s && (*s==' ' || *s=='\t'); s++)
+		;
+	if(strncmp(s, "pathstat/", 9) == 0)
+		h->c->head.fresh_thresh = atoi(s+9);
+	else if(strncmp(s, "have/", 5) == 0)
+		h->c->head.fresh_have = atoi(s+5);
+}
+
+static void
+mimeignore(Hlex *h, char *)
+{
+	lexhead(h);
+}
+
+static void
+parsejump(Hlex *h, char *k)
+{
+	int l, r, m;
+
+	l = 1;
+	r = nelem(mimehead) - 1;
+	while(l <= r){
+		m = (r + l) >> 1;
+		if(cistrcmp(mimehead[m].name, k) <= 0)
+			l = m + 1;
+		else
+			r = m - 1;
+	}
+	m = l - 1;
+	if(cistrcmp(mimehead[m].name, k) == 0 && !mimehead[m].ignore){
+		mimehead[m].seen = 1;
+		(*mimehead[m].parse)(h, k);
+	}else
+		mimeignore(h, k);
+}
+
+static int
+lex(Hlex *h)
+{
+	return h->tok = lex1(h, 0);
+}
+
+static int
+lexbase64(Hlex *h)
+{
+	int c, n;
+
+	n = 0;
+	lex1(h, 1);
+
+	while((c = getc(h)) >= 0){
+		if(!isalnum(c) && c != '+' && c != '/'){
+			ungetc(h);
+			break;
+		}
+		if(n < HMaxWord-1)
+			h->wordval[n++] = c;
+	}
+	h->wordval[n] = '\0';
+	return n;
+}
+
+/*
+ * rfc 822/rfc 1521 lexical analyzer
+ */
+static int
+lex1(Hlex *h, int skipwhite)
+{
+	int level, c;
+
+	if(h->eol)
+		return '\n';
+
+top:
+	c = getc(h);
+	switch(c){
+	case '(':
+		level = 1;
+		while((c = getc(h)) >= 0){
+			if(c == '\\'){
+				c = getc(h);
+				if(c < 0)
+					return '\n';
+				continue;
+			}
+			if(c == '(')
+				level++;
+			else if(c == ')' && --level == 0)
+				break;
+			else if(c == '\n'){
+				c = getc(h);
+				if(c < 0)
+					return '\n';
+				if(c == ')' && --level == 0)
+					break;
+				if(c != ' ' && c != '\t'){
+					ungetc(h);
+					return '\n';
+				}
+			}
+		}
+		goto top;
+
+	case ' ': case '\t':
+		goto top;
+
+	case '\r':
+		c = getc(h);
+		if(c != '\n'){
+			ungetc(h);
+			goto top;
+		}
+
+	case '\n':
+		if(h->tok == '\n'){
+			h->eol = 1;
+			h->eoh = 1;
+			return '\n';
+		}
+		c = getc(h);
+		if(c < 0){
+			h->eol = 1;
+			return '\n';
+		}
+		if(c != ' ' && c != '\t'){
+			ungetc(h);
+			h->eol = 1;
+			return '\n';
+		}
+		goto top;
+
+	case ')':
+	case '<': case '>':
+	case '[': case ']':
+	case '@': case '/':
+	case ',': case ';': case ':': case '?': case '=':
+		if(skipwhite){
+			ungetc(h);
+			return c;
+		}
+		return c;
+
+	case '"':
+		if(skipwhite){
+			ungetc(h);
+			return c;
+		}
+		word(h, "\"");
+		getc(h);		/* skip the closing quote */
+		return QString;
+
+	default:
+		ungetc(h);
+		if(skipwhite)
+			return c;
+		word(h, "\"(){}<>@,;:/[]?=\r\n \t");
+		if(h->wordval[0] == '\0'){
+			h->c->head.closeit = 1;
+			hfail(h->c, HSyntax);
+			longjmp(h->jmp, -1);
+		}
+		return Word;
+	}
+	/* not reached */
+}
+
+/*
+ * return the rest of an rfc 822, including \n
+ * do not map to lower case
+ */
+static void
+lexhead(Hlex *h)
+{
+	int c, n;
+
+	n = 0;
+	while((c = getc(h)) >= 0){
+		if(c == '\r')
+			c = wordcr(h);
+		else if(c == '\n')
+			c = wordnl(h);
+		if(c == '\n')
+			break;
+		if(c == '\\'){
+			c = getc(h);
+			if(c < 0)
+				break;
+		}
+
+		if(n < HMaxWord-1)
+			h->wordval[n++] = c;
+	}
+	h->tok = '\n';
+	h->eol = 1;
+	h->wordval[n] = '\0';
+}
+
+static void
+word(Hlex *h, char *stop)
+{
+	int c, n;
+
+	n = 0;
+	while((c = getc(h)) >= 0){
+		if(c == '\r')
+			c = wordcr(h);
+		else if(c == '\n')
+			c = wordnl(h);
+		if(c == '\\'){
+			c = getc(h);
+			if(c < 0)
+				break;
+		}else if(c < 32 || strchr(stop, c) != nil){
+			ungetc(h);
+			break;
+		}
+
+		if(n < HMaxWord-1)
+			h->wordval[n++] = c;
+	}
+	h->wordval[n] = '\0';
+}
+
+static int
+wordcr(Hlex *h)
+{
+	int c;
+
+	c = getc(h);
+	if(c == '\n')
+		return wordnl(h);
+	ungetc(h);
+	return ' ';
+}
+
+static int
+wordnl(Hlex *h)
+{
+	int c;
+
+	c = getc(h);
+	if(c == ' ' || c == '\t')
+		return c;
+	ungetc(h);
+
+	return '\n';
+}
+
+static int
+getc(Hlex *h)
+{
+	if(h->eoh)
+		return -1;
+	if(h->c->hpos < h->c->hstop)
+		return *h->c->hpos++;
+	h->eoh = 1;
+	h->eol = 1;
+	return -1;
+}
+
+static void
+ungetc(Hlex *h)
+{
+	if(h->eoh)
+		return;
+	h->c->hpos--;
+}
+
+static ulong
+digtoul(char *s, char **e)
+{
+	ulong v;
+	int c, ovfl;
+
+	v = 0;
+	ovfl = 0;
+	for(;;){
+		c = *s;
+		if(c < '0' || c > '9')
+			break;
+		s++;
+		c -= '0';
+		if(v > UlongMax/10 || v == UlongMax/10 && c >= UlongMax%10)
+			ovfl = 1;
+		v = v * 10 + c;
+	}
+
+	if(e)
+		*e = s;
+	if(ovfl)
+		return UlongMax;
+	return v;
+}
+
+int
+http11(HConnect *c)
+{
+	return c->req.vermaj > 1 || c->req.vermaj == 1 && c->req.vermin > 0;
+}
+
+char*
+hmkmimeboundary(HConnect *c)
+{
+	char buf[32];
+	int i;
+
+	srand((time(0)<<16)|getpid());
+	strcpy(buf, "upas-");
+	for(i = 5; i < sizeof(buf)-1; i++)
+		buf[i] = 'a' + nrand(26);
+	buf[i] = 0;
+	return hstrdup(c, buf);
+}
+
+HSPairs*
+hmkspairs(HConnect *c, char *s, char *t, HSPairs *next)
+{
+	HSPairs *sp;
+
+	sp = halloc(c, sizeof *sp);
+	sp->s = s;
+	sp->t = t;
+	sp->next = next;
+	return sp;
+}
+
+HSPairs*
+hrevspairs(HSPairs *sp)
+{
+	HSPairs *last, *next;
+
+	last = nil;
+	for(; sp != nil; sp = next){
+		next = sp->next;
+		sp->next = last;
+		last = sp;
+	}
+	return last;
+}
+
+HFields*
+hmkhfields(HConnect *c, char *s, HSPairs *p, HFields *next)
+{
+	HFields *hf;
+
+	hf = halloc(c, sizeof *hf);
+	hf->s = s;
+	hf->params = p;
+	hf->next = next;
+	return hf;
+}
+
+HFields*
+hrevhfields(HFields *hf)
+{
+	HFields *last, *next;
+
+	last = nil;
+	for(; hf != nil; hf = next){
+		next = hf->next;
+		hf->next = last;
+		last = hf;
+	}
+	return last;
+}
+
+HContent*
+hmkcontent(HConnect *c, char *generic, char *specific, HContent *next)
+{
+	HContent *ct;
+
+	ct = halloc(c, sizeof(HContent));
+	ct->generic = generic;
+	ct->specific = specific;
+	ct->next = next;
+	ct->q = 1;
+	ct->mxb = 0;
+	return ct;
+}
--- /dev/null
+++ b/sys/src/libhttpd/parsereq.c
@@ -1,0 +1,295 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+typedef struct Strings		Strings;
+
+struct Strings
+{
+	char	*s1;
+	char	*s2;
+};
+
+static	char*		abspath(HConnect *cc, char *origpath, char *curdir);
+static	int		getc(HConnect*);
+static	char*		getword(HConnect*);
+static	Strings		parseuri(HConnect *c, char*);
+static	Strings		stripsearch(char*);
+
+/*
+ * parse the next request line
+ * returns:
+ *	1 ok
+ *	0 eof
+ *	-1 error
+ */
+int
+hparsereq(HConnect *c, int timeout)
+{
+	Strings ss;
+	char *vs, *v, *search, *uri, *origuri, *extra;
+
+	if(c->bin != nil){
+		hfail(c, HInternal);
+		return -1;
+	}
+
+	/*
+	 * serve requests until a magic request.
+	 * later requests have to come quickly.
+	 * only works for http/1.1 or later.
+	 */
+	if(timeout)
+		alarm(timeout);
+	if(hgethead(c, 0) < 0)
+		return -1;
+	if(timeout)
+		alarm(0);
+	c->reqtime = time(nil);
+	c->req.meth = getword(c);
+	if(c->req.meth == nil){
+		hfail(c, HSyntax);
+		return -1;
+	}
+	uri = getword(c);
+	if(uri == nil || strlen(uri) == 0){
+		hfail(c, HSyntax);
+		return -1;
+	}
+	v = getword(c);
+	if(v == nil){
+		if(strcmp(c->req.meth, "GET") != 0){
+			hfail(c, HUnimp, c->req.meth);
+			return -1;
+		}
+		c->req.vermaj = 0;
+		c->req.vermin = 9;
+	}else{
+		vs = v;
+		if(strncmp(vs, "HTTP/", 5) != 0){
+			hfail(c, HUnkVers, vs);
+			return -1;
+		}
+		vs += 5;
+		c->req.vermaj = strtoul(vs, &vs, 10);
+		if(*vs != '.' || c->req.vermaj != 1){
+			hfail(c, HUnkVers, vs);
+			return -1;
+		}
+		vs++;
+		c->req.vermin = strtoul(vs, &vs, 10);
+		if(*vs != '\0'){
+			hfail(c, HUnkVers, vs);
+			return -1;
+		}
+
+		extra = getword(c);
+		if(extra != nil){
+			hfail(c, HSyntax);
+			return -1;
+		}
+	}
+
+	/*
+	 * the fragment is not supposed to be sent
+	 * strip it 'cause some clients send it
+	 */
+	origuri = uri;
+	uri = strchr(origuri, '#');
+	if(uri != nil)
+		*uri = 0;
+
+	/*
+	 * http/1.1 requires the server to accept absolute
+	 * or relative uri's.  convert to relative with an absolute path
+	 */
+	if(http11(c)){
+		ss = parseuri(c, origuri);
+		uri = ss.s1;
+		c->req.urihost = ss.s2;
+		if(uri == nil){
+			hfail(c, HBadReq, uri);
+			return -1;
+		}
+		origuri = uri;
+	}
+
+	/*
+	 * munge uri for search, protection, and magic
+	 */
+	ss = stripsearch(origuri);
+	origuri = ss.s1;
+	search = ss.s2;
+	uri = hurlunesc(c, origuri);
+	uri = abspath(c, uri, "/");
+	if(uri == nil || uri[0] == '\0'){
+		hfail(c, HNotFound, "no object specified");
+		return -1;
+	}
+
+	c->req.uri = uri;
+	c->req.search = search;
+	if(search)
+		c->req.searchpairs = hparsequery(c, hstrdup(c, search));
+
+	return 1;
+}
+
+static Strings
+parseuri(HConnect *c, char *uri)
+{
+	Strings ss;
+	char *urihost, *p;
+
+	urihost = nil;
+	ss.s1 = ss.s2 = nil;
+	if(uri[0] != '/')
+		if(cistrncmp(uri, "http://", 7) == 0)
+			uri += 5;		/* skip http: */
+		else if (cistrncmp(uri, "https://", 8) == 0)
+			uri += 6;		/* skip https: */
+		else
+			return ss;
+
+	/*
+	 * anything starting with // is a host name or number
+	 * hostnames consists of letters, digits, - and .
+	 * for now, just ignore any port given
+	 */
+	if(uri[0] == '/' && uri[1] == '/'){
+		urihost = uri + 2;
+		p = strchr(urihost, '/');
+		if(p == nil)
+			uri = hstrdup(c, "/");
+		else{
+			uri = hstrdup(c, p);
+			*p = '\0';
+		}
+		p = strchr(urihost, ':');
+		if(p != nil)
+			*p = '\0';
+	}
+
+	if(uri[0] != '/' || uri[1] == '/')
+		return ss;
+
+	ss.s1 = uri;
+	ss.s2 = hlower(urihost);
+	return ss;
+}
+static Strings
+stripsearch(char *uri)
+{
+	Strings ss;
+	char *search;
+
+	search = strchr(uri, '?');
+	if(search != nil)
+		*search++ = 0;
+	ss.s1 = uri;
+	ss.s2 = search;
+	return ss;
+}
+
+/*
+ *  to circumscribe the accessible files we have to eliminate ..'s
+ *  and resolve all names from the root.
+ */
+static char*
+abspath(HConnect *cc, char *origpath, char *curdir)
+{
+	char *p, *sp, *path, *work, *rpath;
+	int len, n, c;
+
+	if(curdir == nil)
+		curdir = "/";
+	if(origpath == nil)
+		origpath = "";
+	work = hstrdup(cc, origpath);
+	path = work;
+
+	/*
+	 * remove any really special characters
+	 */
+	for(sp = "`;|"; *sp; sp++){
+		p = strchr(path, *sp);
+		if(p)
+			*p = 0;
+	}
+
+	len = strlen(curdir) + strlen(path) + 2 + UTFmax;
+	if(len < 10)
+		len = 10;
+	rpath = halloc(cc, len);
+	if(*path == '/')
+		rpath[0] = 0;
+	else
+		strcpy(rpath, curdir);
+	n = strlen(rpath);
+
+	while(path){
+		p = strchr(path, '/');
+		if(p)
+			*p++ = 0;
+		if(strcmp(path, "..") == 0){
+			while(n > 1){
+				n--;
+				c = rpath[n];
+				rpath[n] = 0;
+				if(c == '/')
+					break;
+			}
+		}else if(strcmp(path, ".") == 0){
+			;
+		}else if(n == 1)
+			n += snprint(rpath+n, len-n, "%s", path);
+		else
+			n += snprint(rpath+n, len-n, "/%s", path);
+		path = p;
+	}
+
+	if(strncmp(rpath, "/bin/", 5) == 0)
+		strcpy(rpath, "/");
+	return rpath;
+}
+
+static char*
+getword(HConnect *c)
+{
+	char *buf;
+	int ch, n;
+
+	while((ch = getc(c)) == ' ' || ch == '\t' || ch == '\r')
+		;
+	if(ch == '\n')
+		return nil;
+	n = 0;
+	buf = halloc(c, 1);
+	for(;;){
+		switch(ch){
+		case ' ':
+		case '\t':
+		case '\r':
+		case '\n':
+			buf[n] = '\0';
+			return hstrdup(c, buf);
+		}
+
+		if(n < HMaxWord-1){
+			buf = bingrow(&c->bin, buf, n, n + 1, 0);
+			if(buf == nil)
+				return nil;
+			buf[n++] = ch;
+		}
+		ch = getc(c);
+	}
+}
+
+static int
+getc(HConnect *c)
+{
+	if(c->hpos < c->hstop)
+		return *c->hpos++;
+	return '\n';
+}
--- /dev/null
+++ b/sys/src/libhttpd/query.c
@@ -1,0 +1,39 @@
+#include <u.h>
+#include <libc.h>
+#include <httpd.h>
+
+/*
+ * parse a search string of the form
+ * tag=val&tag1=val1...
+ */
+HSPairs*
+hparsequery(HConnect *c, char *search)
+{
+	HSPairs *q;
+	char *tag, *val, *s;
+
+	while((s = strchr(search, '?')) != nil)
+		search = s + 1;
+	s = search;
+	while((s = strchr(s, '+')) != nil)
+		*s++ = ' ';
+	q = nil;
+	while(*search){
+		tag = search;
+		while(*search != '='){
+			if(*search == '\0')
+				return q;
+			search++;
+		}
+		*search++ = 0;
+		val = search;
+		while(*search != '&'){
+			if(*search == '\0')
+				return hmkspairs(c, hurlunesc(c, tag), hurlunesc(c, val), q);
+			search++;
+		}
+		*search++ = '\0';
+		q = hmkspairs(c, hurlunesc(c, tag), hurlunesc(c, val), q);
+	}
+	return q;
+}
--- /dev/null
+++ b/sys/src/libhttpd/redirected.c
@@ -1,0 +1,74 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+int
+hredirected(HConnect *c, char *how, char *uri)
+{
+	Hio *hout;
+	char *s, *ss, *scheme, *host;
+	char sayport[NETPATHLEN];
+	int n;
+
+	scheme = c->scheme? c->scheme: "http";
+	host = c->head.host;
+	if(strchr(uri, ':') != nil)
+		host = "";
+	else if(uri[0] != '/'){
+		s = strrchr(c->req.uri, '/');
+		if(s != nil)
+			*s = '\0';
+		ss = halloc(c, strlen(c->req.uri) + strlen(uri) + 2 + UTFmax);
+		sprint(ss, "%s/%s", c->req.uri, uri);
+		uri = ss;
+		if(s != nil)
+			*s = '/';
+	}
+
+	if((strcmp(scheme, "http") == 0 && atoi(c->port) == 80) ||
+	   (strcmp(scheme, "https") == 0 && atoi(c->port) == 443) ||
+	    strchr(host, ':') != nil)
+		sayport[0] = '\0';
+	else
+		snprint(sayport, sizeof sayport, ":%s", c->port);
+
+	n = snprint(c->xferbuf, HBufSize, 
+			"<head><title>Redirection</title></head>\r\n"
+			"<body><h1>Redirection</h1>\r\n"
+			"Your selection can be found <a href=\"%U\"> here</a>.<p></body>\r\n", uri);
+
+	hout = &c->hout;
+	hprint(hout, "%s %s\r\n", hversion, how);
+	hprint(hout, "Date: %D\r\n", time(nil));
+	hprint(hout, "Server: Plan9\r\n");
+	hprint(hout, "Content-type: text/html\r\n");
+	hprint(hout, "Content-Length: %d\r\n", n);
+	if(host == nil || host[0] == 0)
+		hprint(hout, "Location: %U\r\n", uri);
+	else
+		hprint(hout, "Location: %s://%U%s%U\r\n",
+			scheme, host, sayport, uri);
+	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, n);
+
+	if(c->replog)
+		if(host == nil || host[0] == 0)
+			c->replog(c, "Reply: %s\nRedirect: %U\n", how, uri);
+		else
+			c->replog(c, "Reply: %s\nRedirect: %s://%U%s%U\n",
+				how, scheme, host, sayport, uri);
+	return hflush(hout);
+}
+
+int
+hmoved(HConnect *c, char *uri)
+{
+	return hredirected(c, "301 Moved Permanently", uri);
+}
--- /dev/null
+++ b/sys/src/libhttpd/unallowed.c
@@ -1,0 +1,35 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+int
+hunallowed(HConnect *c, char *allowed)
+{
+	Hio *hout;
+	int n;
+
+	n = snprint(c->xferbuf, HBufSize, "<head><title>Method Not Allowed</title></head>\r\n"
+		"<body><h1>Method Not Allowed</h1>\r\n"
+		"You can't %s on <a href=\"%U\"> here</a>.<p></body>\r\n", c->req.meth, c->req.uri);
+
+	hout = &c->hout;
+	hprint(hout, "%s 405 Method Not Allowed\r\n", hversion);
+	hprint(hout, "Date: %D\r\n", time(nil));
+	hprint(hout, "Server: Plan9\r\n");
+	hprint(hout, "Content-Type: text/html\r\n");
+	hprint(hout, "Allow: %s\r\n", allowed);
+	hprint(hout, "Content-Length: %d\r\n", 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)
+		hwrite(hout, c->xferbuf, n);
+
+	if(c->replog)
+		c->replog(c, "Reply: 405 Method Not Allowed\nReason: Method Not Allowed\n");
+	return hflush(hout);
+}
--- /dev/null
+++ b/sys/src/libhttpd/urlfmt.c
@@ -1,0 +1,26 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+int
+hurlfmt(Fmt *f)
+{
+	char buf[HMaxWord*2];
+	Rune r;
+	char *s;
+	int t;
+
+	s = va_arg(f->args, char*);
+	for(t = 0; t < sizeof(buf) - 8; ){
+		s += chartorune(&r, s);
+		if(r == 0)
+			break;
+		if(r <= ' ' || r == '%' || r >= Runeself)
+			t += snprint(&buf[t], sizeof(buf)-t, "%%%2.2x", r);
+		else
+			buf[t++] = r;
+	}
+	buf[t] = 0;
+	return fmtstrcpy(f, buf);
+}
--- /dev/null
+++ b/sys/src/libhttpd/urlunesc.c
@@ -1,0 +1,58 @@
+#include <u.h>
+#include <libc.h>
+#include <bin.h>
+#include <httpd.h>
+
+/* go from url with escaped utf to utf */
+char *
+hurlunesc(HConnect *cc, char *s)
+{
+	char *t, *v, *u;
+	Rune r;
+	int c, n;
+
+	/* unescape */
+	u = halloc(cc, 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<<4)+n;
+		}
+		*t++ = c;
+	}
+	*t = '\0';
+
+	/* convert to valid utf */
+	v = halloc(cc, UTFmax*strlen(u) + 1);
+	s = u;
+	t = v;
+	while(*s){
+		/* in decoding error, assume latin1 */
+		if((n=chartorune(&r, s)) == 1 && r == Runeerror)
+			r = (uchar)*s;
+		s += n;
+		t += runetochar(t, &r);
+	}
+	*t = '\0';
+
+	return v;
+}
--- a/sys/src/mkfile
+++ b/sys/src/mkfile
@@ -19,6 +19,7 @@
 	libframe\
 	libgeometry\
 	libhtml\
+	libhttpd\
 	libip\
 	liblex\
 	libjson\
--