git: 9front

Download patch

ref: 493201f71802df13d224f964fc528de234043c95
parent: 114f77c9faf4a51bc6338e20b1c33f72f91ba1c3
author: cinap_lenrek <cinap_lenrek@rei2.9hal>
date: Wed Jan 11 11:17:54 EST 2012

new webfs, rc based hget

--- /dev/null
+++ b/rc/bin/hget
@@ -1,0 +1,69 @@
+#!/bin/rc
+argv0=$0
+fn usage {
+	echo usage: $argv0 [ -o file ] [ -p body ] [ -r header ] [ -m method ] [ -b baseurl ] url >[2=1]
+	exit usage
+}
+s=0
+o=()
+p=()
+r=()
+m=()
+b=()
+while(~ $1 -*){
+	switch($1){
+	case -o
+		o=$2
+		shift
+	case -p
+		p=$2
+		shift
+	case -r
+		r=($r $2)
+		shift
+	case -m
+		m=$2
+		shift
+	case -b
+		b=$2
+		shift
+	case *
+		usage
+	}
+	shift
+}
+if(! ~ $#* 1)
+	usage
+if(! ~ $#o 0){
+	if(! ~ $#o 1)
+		usage
+	if(test -s $o)
+		s=`{ls -l $o | awk '{print $6}'}
+}
+if(! ~ $s 0)
+	r=($r 'Range: bytes='^$s^'-')
+<>/mnt/web/clone {
+	d=/mnt/web/^`{sed 1q}
+	if(~ $#b 1)
+		echo -n baseurl $b >[1=0]
+	echo -n url $1 >[1=0]
+	for(i in $r)
+		echo -n headers $i >[1=0]
+	if(~ $#m 1)
+		echo -n request $m >[1=0]
+	if(~ $#p 1)
+		cat <$p >$d/postbody
+	<$d/body {
+		if(~ $#o 1){
+			l=`{cat $d/contentlength >[2]/dev/null}
+			x=`{awk 'BEGIN{FS=" |-"}/^bytes ([0-9]+)\-/{print $2}' \
+				$d/contentrange >[2]/dev/null}
+			if(~ $s $l && ~ $#x 0)
+				exit
+			if(~ $s $x)
+				exec cat >>$o
+			exec cat >$o
+		}
+		exec cat
+	}
+}
--- a/sys/lib/dist/usr/glenda/lib/profile
+++ b/sys/lib/dist/usr/glenda/lib/profile
@@ -9,9 +9,15 @@
 fn cd { builtin cd $* && awd }  # for acme
 switch($service){
 case terminal
+	if(! test -w $home/lib/webcookies){
+		touch /tmp/webcookies
+		webcookies -f /tmp/webcookies
+	}
+	if not {
+		webcookies
+	}
+	webfs
 	plumber
-	touch /tmp/webcookies
-	webfs -c /tmp/webcookies
 	echo -n accelerated > '#m/mousectl'
 	echo -n 'res 3' > '#m/mousectl'
 	prompt=('term% ' '	')
--- a/sys/lib/newuser
+++ b/sys/lib/newuser
@@ -10,10 +10,11 @@
 x='$'
 mkdir bin bin/rc bin/mips bin/386 bin/power bin/arm
 mkdir lib tmp
+touch lib/webcookies
+chmod 600 lib/webcookies
 chmod +t tmp
 bind -qc /n/other/usr/$user/tmp $home/tmp
 bind -c $home/tmp /tmp
-
 mail -c
 auth/cron -c
 
@@ -26,6 +27,8 @@
 font = /lib/font/bit/pelm/euro.9.font
 switch($x^service){
 case terminal
+	webcookies
+	webfs
 	plumber
 	startupasfs
 	echo -n accelerated > '#m/mousectl'
--- a/sys/man/1/hget
+++ b/sys/man/1/hget
@@ -4,19 +4,20 @@
 .SH SYNOPSIS
 .B hget
 [
-.B -dhv
-] [
 .B -o
-.I ofile
+.I file
 ] [
 .B -p
 .I body
 ] [
-.B -x
-.I netmntpt
-] [
 .B -r
 .I header
+] [
+.B -m
+.I method
+] [
+.B -b
+.I baseurl
 ]
 .I url
 .SH DESCRIPTION
@@ -26,8 +27,17 @@
 and writes it, absent the
 .B -o
 option, to standard output.
-The known URL types are: http and ftp.
 .PP
+The
+.I url
+can be a relative path like
+.B ../index.html
+if a absolute
+.I baseurl
+was specified with the
+.B -b
+option.
+.PP
 If
 .I url
 is of type HTTP and the
@@ -47,40 +57,22 @@
 will fetch the missing bytes.
 .PP
 Option
-.B -h
-causes HTTP headers to be printed to standard output
-in addition to the transferred web page.
-.PP
-Option
 .B -r
 sends an arbitrary HTTP
 .IR header .
 .PP
 Option
-.B -d
-turns on debugging written to standard error.
-.PP
-Normally,
-.I hget
-uses the IP stack mounted under
-.BR /net .
-The
-.B -x
-option can be used to specify the mount point of
-a different IP stack to use.
-.PP
-Option
-.B -v
-writes progress lines to standard error once a second.
-Each line contains two numbers, the bytes transferred so
-far and the total length to be transferred.
-.PP
-If the environment variable
-.B httpproxy
-is set, it is used as a URL denoting an HTTP proxy server.
-All HTTP accesses use this server to get the page instead of
-calling the destination server.
+.B -m
+overrides the HTTP method used for the request.
 .SH SOURCE
-.B /sys/src/cmd/hget.c
+.B /rc/bin/hget
 .SH "SEE ALSO"
+.IR webfs (4),
 .IR ftpfs (4)
+.SH DIAGNOSTICS
+.I Hget
+requires 
+.IR webfs (4)
+service mounted on
+.B /mnt/web
+to work.
--- a/sys/man/4/webcookies
+++ b/sys/man/4/webcookies
@@ -147,20 +147,8 @@
 .B cookiefs
 decides not to accept the cookie (as outlined in
 RFC2109, section 4.3.4), no indication is given.
-.PP
-.IR Hget (1)
-uses
-.BR /mnt/webcookies/http ,
-when it exists, to manage cookie state.
-.I Webfs
-does not (yet).
 .SH SOURCE
 .B /sys/src/cmd/webcookies.c
 .SH SEE ALSO
+.IR webfs (4),
 .IR hget (1)
-.SH BUGS
-It's not clear what the relationship between
-.I cookiefs
-and something like
-.I webfs
-should be.
--- a/sys/man/4/webfs
+++ b/sys/man/4/webfs
@@ -4,10 +4,6 @@
 .SH SYNOPSIS
 .B webfs
 [
-.B -c
-.I cookiefile
-]
-[
 .B -m
 .I mtpt
 ]
@@ -26,10 +22,15 @@
 .BR /mnt/web ),
 and, if 
 .I service
-is specified, will post a service file descriptor
-in 
+is specified, will post a service file descriptor in 
 .BR /srv/\fIservice .
 .PP
+If the enviroment variable
+.B httpproxy
+is set, all HTTP request initiated by
+.I webfs
+will be made thru that proxy url.
+.PP
 .I Webfs
 presents a three-level file system suggestive
 of the network protocol hierarchies
@@ -37,13 +38,12 @@
 and
 .IR ether (3).
 .PP
-The top level contains three files:
+The top level contains the files files:
 .BR ctl ,
-.BR cookies ,
 and
 .BR clone .
 .PP
-The
+The top level
 .B ctl
 file is used to maintain parameters global to the instance of
 .IR webfs .
@@ -53,72 +53,6 @@
 Writing strings of the form
 .RB `` attr " " value ''
 sets a particular attribute.
-Attributes are:
-.TP
-.B chatty9p
-The
-.B chatty9p
-flag used by the 9P library, discussed in
-.IR 9p (2).
-.B 0
-is no debugging,
-.B 1
-prints 9P message traces on standard error,
-and values above
-.B 1
-present more debugging, at the whim of the library.
-The default for this and the following debug flags is 
-.BR 0 .
-.TP
-.B fsdebug
-This variable is the level of debugging output about the file system module.
-.TP
-.B cookiedebug
-This variable is the level of debugging output about the cookie module.
-.TP
-.B urldebug
-This variable is the level of debugging output about URL parsing.
-.TP
-.B acceptcookies
-This flag controls whether to accept cookies presented by remote web servers.
-(Cookies are described below, in the discussion of the
-.B cookies
-file.)
-The values
-.B on
-and
-.B off
-are synonymous with
-.B 1
-and
-.BR 0 .
-The default is
-.BR on .
-.TP
-.B sendcookies
-This flag controls whether to present stored cookies to remote web servers.
-The default is
-.BR on .
-.TP
-.B redirectlimit
-Web servers can respond to a request with a message
-redirecting to another page.
-.I Webfs
-makes no effort to determine whether it is in an infinite
-redirect loop.
-Instead, it gives up after this many redirects.
-The default is
-.BR 10 .
-.TP
-.B useragent
-.I Webfs
-sends the value of this attribute in its
-.B User-Agent:
-header in its HTTP requests.
-The default is
-.RB `` "webfs/2.0 (plan 9)" .''
-.PD
-.PP
 The top-level directory also contains
 numbered directories corresponding to connections, which
 may be used to fetch a single URL.
@@ -131,23 +65,10 @@
 .B clone
 file is equivalent to the file
 .IB n /ctl \fR.
-A connection is assumed closed once all files in its directory
-have been closed, and is then will be reallocated.
+A connection is assumed closed once all files in its
+directory have been closed, and is then will be reallocated.
 .PP
-Each connection has its own private set of
-.BR acceptcookies ,
-.BR sendcookies ,
-.BR redirectlimit ,
-and
-.B useragent
-variables, initialized to the defaults set in the
-root's
-.B ctl
-file.  The per-connection
-.B ctl
-file allows editing the variables for this particular connection.
-.PP
-Each connection also has a URL string variable
+Each connection has a URL attribute
 .B url
 associated with it.
 This URL may be an absolute URL such as
@@ -156,153 +77,96 @@
 .IR ../index.html .
 The
 .B baseurl
-string variable sets the URL against which relative URLs
+attribute sets the URL against which relative URLs
 are interpreted.
-Once the URL has been set,
-its pieces can be retrieved via individual files in the
+Once the URL has been set by wrting to the
+.B ctl
+file of the connetcion, its pieces can be retrieved via
+individual files in the
 .B parsed
-directory.
-.I Webfs
-parses the following URL syntaxes; names in italics are
-the names of files in the
-.B parsed
-directory.
-.IP
-\fIscheme\f5:\fIschemedata
-.br
-\f5http://\fIhost\f5/\fIpath\fR[\f5?\fIquery\fR][\f5#\fIfragment\fR]
-.br
-\f5ftp://\fR[\fIuser\fR[\f5:\fIpassword\fR]\f5@\fR]\fP\f5\fIhost\f5/\fIpath\fR[\f5;type=\fIftptype\fR]
-.br
-\f5file:\fIpath
-.LP
-If there is associated data to be
-posted with the request, it can be written to
+directory:
+.de UU
+.TP
+.B parsed/\fI\\$1
+\\$2
+..
+.UU url http://pete:secret@www.example.com:8000/cgi/search?q=kittens#results
+.UU scheme http
+.UU user pete
+.UU pass secret
+.UU host www.example.com
+.UU port 8000
+.UU path /cgi/search
+.UU query q=kittens
+.UU fragment results
+.PP
+If there is associated data to be posted with the request,
+it can be written to
 .BR postbody .
-Finally, opening
+Opening
+.B postbody
+or
 .B body
-initiates the request.
+initiates the request. If the request fails,
+then opening the
+.B body
+or writing to
+.B postbody
+file will fail and return a error string.
+.PP
+When the
+.B body
+file has been opend, response headers appear
+as files in the connection directory. For example
+reading the
+.B contenttype
+file yields the MIME content type of the body data.
+If the request was redirected, the URL represended
+by the
+.B parsed
+directory will change to the final destination.
+.PP
 The resulting data may be read from
 .B body
 as it arrives.
-After the request has been executed, the MIME content type
-may be read from the
-.B contenttype
-file.
 .PP
-The top-level
-.B cookies
-file contains the internal set of HTTP cookies, which
-are used by HTTP servers to associate requests with persistent
-state such as user profiles.
-It may be edited as an ordinary text file.
-Multiple instances of
-.I webfs
-and
-.IR webcookies (4)
-share cookies by keeping their internal set
-consistent with the
-.I cookiefile
-(default
-.BR $home/lib/webcookies ),
-which has the same format.
-.PP
-These files contain one line per cookie;
-each cookie comprises some number of
-.IB attr = value
-pairs.
-Cookie attributes are:
+The following is a list of attributes that can be
+set to to a connection prior initiating the request:
 .TP
-.BI name= name
-The name of the cookie on the remote server.
+.B url,baseurl
+See above.
 .TP
-.BI value= value
-The value associated with that name on the remote server.
-The actual data included when a cookie is sent back
-to the server is
-.IB \fR``\fIname = value\fR''
-(where, confusingly,
-.I name
-and
-.I value
-are the values associated with the
-.B name
-and
-.B value
-attributes.
+.B useragent
+Sets a custom useragent string to be used with the request.
 .TP
-.BI domain= domain
-If
-.I domain
-is an IP address, the cookie can only be used for URLs
-with
-.I host
-equal to that IP address.
-Otherwise,
-.I domain
-must be a pattern beginning with a dot, and
-the cookie can only be used for URLs with a
-.I host
-having
-.I domain
-as a suffix.
-For example, a cookie with
-.B domain=.bell-labs.com
-may be used on hosts
-.I www.bell-labs.com
-and
-.IR www.research.bell-labs.com
-(but not
-.IR www.not-bell-labs.com ).
+.B contenttype
+Sets the MIME content type of the postbody.
 .TP
-.BI path= path
-The cookie can only be used for URLs with a path
-beginning with
-.IR path .
+.B request
+Usualy, the HTTP method used is
+.B POST
+when
+.B postbody
+file is opend first or
+.B GET
+otherwise. This can be overriden with the
+.B request
+attribute so send arbitrary HTTP requests.
 .TP
-.BI version= version
-The version of the HTTP cookie specification, specified by the server.
-.TP
-.BI comment= comment
-A comment, specified by the server.
-.TP
-.BI expire= expire
-The cookie expires at time
-.IR expire ,
-which is a decimal number of seconds since the epoch.
-.TP
-.B secure=1
-The cookie may only be used over secure
-.RB ( https )
-connections.
-Secure connections are currently unimplemented.
-.TP
-.B explicitdomain=1
-The domain associated with this cookie was set by
-the server (rather than inferred from a URL).
-.TP
-.B explicitpath=1
-The path associated with this cookie was set by the
-server (rather than inferred from a URL).
-.TP
-.B netscapestyle=1
-The server presented the cookie in ``Netscape style,'' which
-does not conform to the cookie standard, RFC2109.
-It is assumed that when presenting the cookie to the server,
-it must be sent back in Netscape style as well.
-.PD
+.B headers
+Adds arbitrary HTTP headers to be send with
+the request.
 .SH EXAMPLE
-.B /sys/src/cmd/webfs/webget.c
+.B /rc/bin/hget
 is a simple client.
 .SH SOURCE
 .B /sys/src/cmd/webfs
-.SH SEE ALSO
-.IR hget (1),
-.IR webcookies (4)
-.SH BUGS
-It's not clear what the relationship between
-.IR hget ,
-.I webcookies
-and
-.I webfs
-should be.
+.SH "SEE ALSO"
+.IR webcookies (4),
+.IR hget (1)
+.SH DIAGNOSTICS
+For cookies to work,
+.IR webcookies (4),
+should be running and mounted on
+.B /mnt/webcookies
+otherwise cookies will be ignored.
--- a/sys/src/cmd/hget.c
+++ /dev/null
@@ -1,1480 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <ctype.h>
-#include <bio.h>
-#include <ip.h>
-#include <libsec.h>
-#include <auth.h>
-
-typedef struct URL URL;
-struct URL
-{
-	int	method;
-	char	*host;
-	char	*port;
-	char	*page;
-	char	*etag;
-	char	*redirect;
-	char	*postbody;
-	char	*cred;
-	char *rhead;
-	long	mtime;
-};
-
-typedef struct Range Range;
-struct Range
-{
-	long	start;	/* only 2 gig supported, tdb */
-	long	end;
-};
-
-typedef struct Out Out;
-struct Out
-{
-	int fd;
-	int offset;				/* notional current offset in output */
-	int written;			/* number of bytes successfully transferred to output */
-	DigestState *curr;		/* digest state up to offset (if known) */
-	DigestState *hiwat;		/* digest state of all bytes written */
-};
-
-enum
-{
-	Other,
-	Http,
-	Https,
-	Ftp,
-};
-
-enum
-{
-	Eof = 0,
-	Error = -1,
-	Server = -2,
-	Changed = -3,
-};
-
-int debug;
-char *ofile;
-
-
-int	doftp(URL*, URL*, Range*, Out*, long);
-int	dohttp(URL*, URL*,  Range*, Out*, long);
-int	crackurl(URL*, char*);
-Range*	crackrange(char*);
-int	getheader(int, char*, int);
-int	httpheaders(int, int, URL*, Range*);
-int	httprcode(int);
-int	cistrncmp(char*, char*, int);
-int	cistrcmp(char*, char*);
-void	initibuf(void);
-int	readline(int, char*, int);
-int	readibuf(int, char*, int);
-int	dfprint(int, char*, ...);
-void	unreadline(char*);
-int	output(Out*, char*, int);
-void	setoffset(Out*, int);
-
-int	verbose;
-char	*net;
-char	tcpdir[NETPATHLEN];
-int	headerprint;
-
-struct {
-	char	*name;
-	int	(*f)(URL*, URL*, Range*, Out*, long);
-} method[] = {
-	[Http]	{ "http",	dohttp },
-	[Https]	{ "https",	dohttp },
-	[Ftp]	{ "ftp",	doftp },
-	[Other]	{ "_______",	nil },
-};
-
-void
-usage(void)
-{
-	fprint(2, "usage: %s [-dhv] [-o outfile] [-p body] [-x netmtpt] [-r header] url\n", argv0);
-	exits("usage");
-}
-
-void
-main(int argc, char **argv)
-{
-	URL u;
-	Range r;
-	int errs, n;
-	ulong mtime;
-	Dir *d;
-	char postbody[4096], *p, *e, *t, *hpx;
-	URL px; // Proxy
-	Out out;
-
-	ofile = nil;
-	p = postbody;
-	e = p + sizeof(postbody);
-	r.start = 0;
-	r.end = -1;
-	mtime = 0;
-	memset(&u, 0, sizeof(u));
-	memset(&px, 0, sizeof(px));
-	hpx = getenv("httpproxy");
-
-	ARGBEGIN {
-	case 'o':
-		ofile = EARGF(usage());
-		break;
-	case 'd':
-		debug = 1;
-		break;
-	case 'h':
-		headerprint = 1;
-		break;
-	case 'v':
-		verbose = 1;
-		break;
-	case 'x':
-		net = EARGF(usage());
-		break;
-	case 'r':
-		u.rhead = EARGF(usage());
-		break;
-	case 'p':
-		t = EARGF(usage());
-		if(p != postbody)
-			p = seprint(p, e, "&%s", t);
-		else
-			p = seprint(p, e, "%s", t);
-		u.postbody = postbody;
-		
-		break;
-	default:
-		usage();
-	} ARGEND;
-
-	if(net != nil){
-		if(strlen(net) > sizeof(tcpdir)-5)
-			sysfatal("network mount point too long");
-		snprint(tcpdir, sizeof(tcpdir), "%s/tcp", net);
-	} else
-		snprint(tcpdir, sizeof(tcpdir), "tcp");
-
-	if(argc != 1)
-		usage();
-
-	
-	out.fd = 1;
-	out.written = 0;
-	out.offset = 0;
-	out.curr = nil;
-	out.hiwat = nil;
-	if(ofile != nil){
-		d = dirstat(ofile);
-		if(d == nil){
-			out.fd = create(ofile, OWRITE, 0664);
-			if(out.fd < 0)
-				sysfatal("creating %s: %r", ofile);
-		} else {
-			out.fd = open(ofile, OWRITE);
-			if(out.fd < 0)
-				sysfatal("can't open %s: %r", ofile);
-			r.start = d->length;
-			mtime = d->mtime;
-			free(d);
-		}
-	}
-
-	errs = 0;
-
-	if(crackurl(&u, argv[0]) < 0)
-		sysfatal("%r");
-	if(hpx && crackurl(&px, hpx) < 0)
-		sysfatal("%r");
-
-	for(;;){
-		setoffset(&out, 0);
-		/* transfer data */
-		werrstr("");
-		n = (*method[u.method].f)(&u, &px, &r, &out, mtime);
-
-		switch(n){
-		case Eof:
-			exits(0);
-			break;
-		case Error:
-			if(errs++ < 10)
-				continue;
-			sysfatal("too many errors with no progress %r");
-			break;
-		case Server:
-			sysfatal("server returned: %r");
-			break;
-		}
-
-		/* forward progress */
-		errs = 0;
-		r.start += n;
-		if(r.start >= r.end)
-			break;
-	}
-
-	exits(0);
-}
-
-int
-crackurl(URL *u, char *s)
-{
-	char *p;
-	int i;
-
-	if(u->page != nil){
-		free(u->page);
-		u->page = nil;
-	}
-
-	/* get type */ 
-	for(p = s; *p; p++){
-		if(*p == '/'){
-			p = s;
-			if(u->method == Other){
-				werrstr("missing method");
-				return -1;
-			}
-			if(u->host == nil){
-				werrstr("missing host");
-				return -1;
-			}
-			u->page = strdup(p);
-			return 0;
-		}
-		if(*p == ':' && *(p+1)=='/' && *(p+2)=='/'){
-			*p = 0;
-			p += 3;
-			for(i = 0; i < nelem(method); i++){
-				if(cistrcmp(s, method[i].name) == 0){
-					u->method = i;
-					break;
-				}
-			}
-			break;
-		}
-	}
-
-	if(u->method == Other){
-		werrstr("unsupported URL type %s", s);
-		return -1;
-	}
-
-	/* get system */
-	free(u->host);
-	s = p;
-	p = strchr(s, '/');
-	if(p == nil){
-		u->host = strdup(s);
-		u->page = strdup("/");
-	} else {
-		u->page = strdup(p);
-		*p = 0;
-		u->host = strdup(s);
-		*p = '/';
-	}
-
-	if(p = strchr(u->host, ':')) {
-		*p++ = 0;
-		u->port = p;
-	} else 
-		u->port = method[u->method].name;
-
-	if(*(u->host) == 0){
-		werrstr("bad url, null host");
-		return -1;
-	}
-
-	return 0;
-}
-
-char *day[] = {
-	"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
-};
-
-char *month[] = {
-	"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
-};
-
-struct
-{
-	int	fd;
-	long	mtime;
-} note;
-
-void
-catch(void*, char*)
-{
-	Dir d;
-
-	nulldir(&d);
-	d.mtime = note.mtime;
-	if(dirfwstat(note.fd, &d) < 0)
-		sysfatal("catch: can't dirfwstat: %r");
-	noted(NDFLT);
-}
-
-int
-dohttp(URL *u, URL *px, Range *r, Out *out, long mtime)
-{
-	int fd, cfd;
-	int redirect, auth, loop;
-	int n, rv, code;
-	long tot, vtime;
-	Tm *tm;
-	char buf[1024];
-	char err[ERRMAX];
-
-
-	/*  always move back to a previous 512 byte bound because some
-	 *  servers can't seem to deal with requests that start at the
-	 *  end of the file
-	 */
-	if(r->start)
-		r->start = ((r->start-1)/512)*512;
-
-	/* loop for redirects, requires reading both response code and headers */
-	fd = -1;
-	for(loop = 0; loop < 32; loop++){
-		if(px->host == nil){
-			fd = dial(netmkaddr(u->host, tcpdir, u->port), 0, 0, 0);
-		} else {
-			fd = dial(netmkaddr(px->host, tcpdir, px->port), 0, 0, 0);
-		}
-		if(fd < 0)
-			return Error;
-
-		if(u->method == Https){
-			int tfd;
-			TLSconn conn;
-
-			memset(&conn, 0, sizeof conn);
-			tfd = tlsClient(fd, &conn);
-			if(tfd < 0){
-				fprint(2, "tlsClient: %r\n");
-				close(fd);
-				return Error;
-			}
-			/* BUG: check cert here? */
-			if(conn.cert)
-				free(conn.cert);
-			close(fd);
-			fd = tfd;
-		}
-
-		/* write request, use range if not start of file */
-		if(u->postbody == nil){
-			if(px->host == nil){
-				dfprint(fd,	"GET %s HTTP/1.0\r\n"
-						"Host: %s\r\n"
-						"User-agent: Plan9/hget\r\n"
-						"Cache-Control: no-cache\r\n"
-						"Pragma: no-cache\r\n",
-						u->page, u->host);
-			} else {
-				dfprint(fd,	"GET http://%s%s HTTP/1.0\r\n"
-						"Host: %s\r\n"
-						"User-agent: Plan9/hget\r\n"
-						"Cache-Control: no-cache\r\n"
-						"Pragma: no-cache\r\n",
-						u->host, u->page, u->host);
-			}
-		} else {
-			dfprint(fd,	"POST %s HTTP/1.0\r\n"
-					"Host: %s\r\n"
-					"Content-type: application/x-www-form-urlencoded\r\n"
-					"Content-length: %d\r\n"
-					"User-agent: Plan9/hget\r\n",
-					u->page, u->host, strlen(u->postbody));
-		}
-		if(u->cred)
-			dfprint(fd, "Authorization: Basic %s\r\n", u->cred);
-		if(u->rhead)
-			dfprint(fd, "%s\r\n", u->rhead);
-		if(r->start != 0){
-			dfprint(fd, "Range: bytes=%d-\n", r->start);
-			if(u->etag != nil){
-				dfprint(fd, "If-range: %s\n", u->etag);
-			} else {
-				tm = gmtime(mtime);
-				dfprint(fd, "If-range: %s, %d %s %d %2d:%2.2d:%2.2d GMT\n",
-					day[tm->wday], tm->mday, month[tm->mon],
-					tm->year+1900, tm->hour, tm->min, tm->sec);
-			}
-		}
-		if((cfd = open("/mnt/webcookies/http", ORDWR)) >= 0){
-			if(fprint(cfd, "http://%s%s", u->host, u->page) > 0){
-				while((n = read(cfd, buf, sizeof buf)) > 0){
-					if(debug)
-						write(2, buf, n);
-					write(fd, buf, n);
-				}
-			}else{
-				close(cfd);
-				cfd = -1;
-			}
-		}
-			
-		dfprint(fd, "\r\n", u->host);
-		if(u->postbody)
-			dfprint(fd,	"%s", u->postbody);
-
-		auth = 0;
-		redirect = 0;
-		initibuf();
-		code = httprcode(fd);
-		switch(code){
-		case Error:	/* connection timed out */
-		case Eof:
-			close(fd);
-			close(cfd);
-			return code;
-
-		case 200:	/* OK */
-		case 201:	/* Created */
-		case 202:	/* Accepted */
-			if(ofile == nil && r->start != 0)
-				sysfatal("page changed underfoot");
-			break;
-
-		case 204:	/* No Content */
-			sysfatal("No Content");
-
-		case 206:	/* Partial Content */
-			setoffset(out, r->start);
-			break;
-
-		case 301:	/* Moved Permanently */
-		case 302:	/* Moved Temporarily (actually Found) */
-		case 303:	/* See Other */
-		case 307:	/* Temporary Redirect (HTTP/1.1) */
-			redirect = 1;
-			u->postbody = nil;
-			break;
-
-		case 304:	/* Not Modified */
-			break;
-
-		case 400:	/* Bad Request */
-			sysfatal("Bad Request");
-
-		case 401:	/* Unauthorized */
-			if (auth)
-				sysfatal("Authentication failed");
-			auth = 1;
-			break;
-
-		case 402:	/* ??? */
-			sysfatal("Unauthorized");
-
-		case 403:	/* Forbidden */
-			sysfatal("Forbidden by server");
-
-		case 404:	/* Not Found */
-			sysfatal("Not found on server");
-
-		case 407:	/* Proxy Authentication */
-			sysfatal("Proxy authentication required");
-
-		case 500:	/* Internal server error */
-			sysfatal("Server choked");
-
-		case 501:	/* Not implemented */
-			sysfatal("Server can't do it!");
-
-		case 502:	/* Bad gateway */
-			sysfatal("Bad gateway");
-
-		case 503:	/* Service unavailable */
-			sysfatal("Service unavailable");
-		
-		default:
-			sysfatal("Unknown response code %d", code);
-		}
-
-		if(u->redirect != nil){
-			free(u->redirect);
-			u->redirect = nil;
-		}
-
-		rv = httpheaders(fd, cfd, u, r);
-		close(cfd);
-		if(rv != 0){
-			close(fd);
-			return rv;
-		}
-
-		if(!redirect && !auth)
-			break;
-
-		if (redirect){
-			if(u->redirect == nil)
-				sysfatal("redirect: no URL");
-			if(crackurl(u, u->redirect) < 0)
-				sysfatal("redirect: %r");
-		}
-	}
-
-	/* transfer whatever you get */
-	if(ofile != nil && u->mtime != 0){
-		note.fd = out->fd;
-		note.mtime = u->mtime;
-		notify(catch);
-	}
-
-	tot = 0;
-	vtime = 0;
-	for(;;){
-		n = readibuf(fd, buf, sizeof(buf));
-		if(n <= 0)
-			break;
-		if(output(out, buf, n) != n)
-			break;
-		tot += n;
-		if(verbose && (vtime != time(0) || r->start == r->end)) {
-			vtime = time(0);
-			fprint(2, "%ld %ld\n", r->start+tot, r->end);
-		}
-	}
-	notify(nil);
-	close(fd);
-
-	if(ofile != nil && u->mtime != 0){
-		Dir d;
-
-		rerrstr(err, sizeof err);
-		nulldir(&d);
-		d.mtime = u->mtime;
-		if(dirfwstat(out->fd, &d) < 0)
-			fprint(2, "couldn't set mtime: %r\n");
-		errstr(err, sizeof err);
-	}
-
-	return tot;
-}
-
-/* get the http response code */
-int
-httprcode(int fd)
-{
-	int n;
-	char *p;
-	char buf[256];
-
-	n = readline(fd, buf, sizeof(buf)-1);
-	if(n <= 0)
-		return n;
-	if(debug)
-		fprint(2, "%d <- %s\n", fd, buf);
-	p = strchr(buf, ' ');
-	if(strncmp(buf, "HTTP/", 5) != 0 || p == nil){
-		werrstr("bad response from server");
-		return -1;
-	}
-	buf[n] = 0;
-	return atoi(p+1);
-}
-
-/* read in and crack the http headers, update u and r */
-void	hhetag(char*, URL*, Range*);
-void	hhmtime(char*, URL*, Range*);
-void	hhclen(char*, URL*, Range*);
-void	hhcrange(char*, URL*, Range*);
-void	hhuri(char*, URL*, Range*);
-void	hhlocation(char*, URL*, Range*);
-void	hhauth(char*, URL*, Range*);
-
-struct {
-	char *name;
-	void (*f)(char*, URL*, Range*);
-} headers[] = {
-	{ "etag:", hhetag },
-	{ "last-modified:", hhmtime },
-	{ "content-length:", hhclen },
-	{ "content-range:", hhcrange },
-	{ "uri:", hhuri },
-	{ "location:", hhlocation },
-	{ "WWW-Authenticate:", hhauth },
-};
-int
-httpheaders(int fd, int cfd, URL *u, Range *r)
-{
-	char buf[2048];
-	char *p;
-	int i, n;
-
-	for(;;){
-		n = getheader(fd, buf, sizeof(buf));
-		if(n <= 0)
-			break;
-		if(cfd >= 0)
-			fprint(cfd, "%s\n", buf);
-		for(i = 0; i < nelem(headers); i++){
-			n = strlen(headers[i].name);
-			if(cistrncmp(buf, headers[i].name, n) == 0){
-				/* skip field name and leading white */
-				p = buf + n;
-				while(*p == ' ' || *p == '\t')
-					p++;
-
-				(*headers[i].f)(p, u, r);
-				break;
-			}
-		}
-	}
-	return n;
-}
-
-/*
- *  read a single mime header, collect continuations.
- *
- *  this routine assumes that there is a blank line twixt
- *  the header and the message body, otherwise bytes will
- *  be lost.
- */
-int
-getheader(int fd, char *buf, int n)
-{
-	char *p, *e;
-	int i;
-
-	n--;
-	p = buf;
-	for(e = p + n; ; p += i){
-		i = readline(fd, p, e-p);
-		if(i < 0)
-			return i;
-
-		if(p == buf){
-			/* first line */
-			if(strchr(buf, ':') == nil)
-				break;		/* end of headers */
-		} else {
-			/* continuation line */
-			if(*p != ' ' && *p != '\t'){
-				unreadline(p);
-				*p = 0;
-				break;		/* end of this header */
-			}
-		}
-	}
-	if(headerprint)
-		print("%s\n", buf);
-
-	if(debug)
-		fprint(2, "%d <- %s\n", fd, buf);
-	return p-buf;
-}
-
-void
-hhetag(char *p, URL *u, Range*)
-{
-	if(u->etag != nil){
-		if(strcmp(u->etag, p) != 0)
-			sysfatal("file changed underfoot");
-	} else
-		u->etag = strdup(p);
-}
-
-char*	monthchars = "janfebmaraprmayjunjulaugsepoctnovdec";
-
-void
-hhmtime(char *p, URL *u, Range*)
-{
-	char *month, *day, *yr, *hms;
-	char *fields[6];
-	Tm tm, now;
-	int i;
-
-	i = getfields(p, fields, 6, 1, " \t");
-	if(i < 5)
-		return;
-
-	day = fields[1];
-	month = fields[2];
-	yr = fields[3];
-	hms = fields[4];
-
-	/* default time */
-	now = *gmtime(time(0));
-	tm = now;
-	tm.yday = 0;
-
-	/* convert ascii month to a number twixt 1 and 12 */
-	if(*month >= '0' && *month <= '9'){
-		tm.mon = atoi(month) - 1;
-		if(tm.mon < 0 || tm.mon > 11)
-			tm.mon = 5;
-	} else {
-		for(p = month; *p; p++)
-			*p = tolower(*p);
-		for(i = 0; i < 12; i++)
-			if(strncmp(&monthchars[i*3], month, 3) == 0){
-				tm.mon = i;
-				break;
-			}
-	}
-
-	tm.mday = atoi(day);
-
-	if(hms) {
-		tm.hour = strtoul(hms, &p, 10);
-		if(*p == ':') {
-			p++;
-			tm.min = strtoul(p, &p, 10);
-			if(*p == ':') {
-				p++;
-				tm.sec = strtoul(p, &p, 10);
-			}
-		}
-		if(tolower(*p) == 'p')
-			tm.hour += 12;
-	}
-
-	if(yr) {
-		tm.year = atoi(yr);
-		if(tm.year >= 1900)
-			tm.year -= 1900;
-	} else {
-		if(tm.mon > now.mon || (tm.mon == now.mon && tm.mday > now.mday+1))
-			tm.year--;
-	}
-
-	strcpy(tm.zone, "GMT");
-	/* convert to epoch seconds */
-	u->mtime = tm2sec(&tm);
-}
-
-void
-hhclen(char *p, URL*, Range *r)
-{
-	r->end = atoi(p);
-}
-
-void
-hhcrange(char *p, URL*, Range *r)
-{
-	char *x;
-	vlong l;
-
-	l = 0;
-	x = strchr(p, '/');
-	if(x)
-		l = atoll(x+1);
-	if(l == 0) {
-		x = strchr(p, '-');
-		if(x)
-			l = atoll(x+1);
-	}
-	if(l)
-		r->end = l;
-}
-
-void
-hhuri(char *p, URL *u, Range*)
-{
-	if(*p != '<')
-		return;
-	u->redirect = strdup(p+1);
-	p = strchr(u->redirect, '>');
-	if(p != nil)
-		*p = 0;
-}
-
-void
-hhlocation(char *p, URL *u, Range*)
-{
-	u->redirect = strdup(p);
-}
-
-void
-hhauth(char *p, URL *u, Range*)
-{
-	char *f[4];
-	UserPasswd *up;
-	char *s, cred[64];
-
-	if (cistrncmp(p, "basic ", 6) != 0)
-		sysfatal("only Basic authentication supported");
-
-	if (gettokens(p, f, nelem(f), "\"") < 2)
-		sysfatal("garbled auth data");
-
-	if ((up = auth_getuserpasswd(auth_getkey, "proto=pass service=http server=%q realm=%q",
-	    	u->host, f[1])) == nil)
-			sysfatal("cannot authenticate");
-
-	s = smprint("%s:%s", up->user, up->passwd);
-	if(enc64(cred, sizeof(cred), (uchar *)s, strlen(s)) == -1)
-		sysfatal("enc64");
-  		free(s);
-
-	assert(u->cred = strdup(cred));
-}
-
-enum
-{
-	/* ftp return codes */
-	Extra=		1,
-	Success=	2,
-	Incomplete=	3,
-	TempFail=	4,
-	PermFail=	5,
-
-	Nnetdir=	64,	/* max length of network directory paths */
-	Ndialstr=	64,		/* max length of dial strings */
-};
-
-int ftpcmd(int, char*, ...);
-int ftprcode(int, char*, int);
-int hello(int);
-int logon(int);
-int xfertype(int, char*);
-int passive(int, URL*);
-int active(int, URL*);
-int ftpxfer(int, Out*, Range*);
-int terminateftp(int, int);
-int getaddrport(char*, uchar*, uchar*);
-int ftprestart(int, Out*, URL*, Range*, long);
-
-int
-doftp(URL *u, URL *px, Range *r, Out *out, long mtime)
-{
-	int pid, ctl, data, rv;
-	Waitmsg *w;
-	char msg[64];
-	char conndir[NETPATHLEN];
-	char *p;
-
-	/* untested, proxy doesn't work with ftp (I think) */
-	if(px->host == nil){
-		ctl = dial(netmkaddr(u->host, tcpdir, u->port), 0, conndir, 0);
-	} else {
-		ctl = dial(netmkaddr(px->host, tcpdir, px->port), 0, conndir, 0);
-	}
-
-	if(ctl < 0)
-		return Error;
-	if(net == nil){
-		p = strrchr(conndir, '/');
-		*p = 0;
-		snprint(tcpdir, sizeof(tcpdir), conndir);
-	}
-
-	initibuf();
-
-	rv = hello(ctl);
-	if(rv < 0)
-		return terminateftp(ctl, rv);
-
-	rv = logon(ctl);
-	if(rv < 0)
-		return terminateftp(ctl, rv);
-
-	rv = xfertype(ctl, "I");
-	if(rv < 0)
-		return terminateftp(ctl, rv);
-
-	/* if file is up to date and the right size, stop */
-	if(ftprestart(ctl, out, u, r, mtime) > 0){
-		close(ctl);
-		return Eof;
-	}
-		
-	/* first try passive mode, then active */
-	data = passive(ctl, u);
-	if(data < 0){
-		data = active(ctl, u);
-		if(data < 0)
-			return Error;
-	}
-
-	/* fork */
-	switch(pid = rfork(RFPROC|RFFDG|RFMEM)){
-	case -1:
-		close(data);
-		return terminateftp(ctl, Error);
-	case 0:
-		ftpxfer(data, out, r);
-		close(data);
-		_exits(0);
-	default:
-		close(data);
-		break;
-	}
-
-	/* wait for reply message */
-	rv = ftprcode(ctl, msg, sizeof(msg));
-	close(ctl);
-
-	/* wait for process to terminate */
-	w = nil;
-	for(;;){
-		free(w);
-		w = wait();
-		if(w == nil)
-			return Error;
-		if(w->pid == pid){
-			if(w->msg[0] == 0){
-				free(w);
-				break;
-			}
-			werrstr("xfer: %s", w->msg);
-			free(w);
-			return Error;
-		}
-	}
-
-	switch(rv){
-	case Success:
-		return Eof;
-	case TempFail:
-		return Server;
-	default:
-		return Error;
-	}
-}
-
-int
-ftpcmd(int ctl, char *fmt, ...)
-{
-	va_list arg;
-	char buf[2*1024], *s;
-
-	va_start(arg, fmt);
-	s = vseprint(buf, buf + (sizeof(buf)-4) / sizeof(*buf), fmt, arg);
-	va_end(arg);
-	if(debug)
-		fprint(2, "%d -> %s\n", ctl, buf);
-	*s++ = '\r';
-	*s++ = '\n';
-	if(write(ctl, buf, s - buf) != s - buf)
-		return -1;
-	return 0;
-}
-
-int
-ftprcode(int ctl, char *msg, int len)
-{
-	int rv;
-	int i;
-	char *p;
-
-	len--;	/* room for terminating null */
-	for(;;){
-		*msg = 0;
-		i = readline(ctl, msg, len);
-		if(i < 0)
-			break;
-		if(debug)
-			fprint(2, "%d <- %s\n", ctl, msg);
-
-		/* stop if not a continuation */
-		rv = strtol(msg, &p, 10);
-		if(rv >= 100 && rv < 600 && p==msg+3 && *p == ' ')
-			return rv/100;
-	}
-	*msg = 0;
-
-	return -1;
-}
-
-int
-hello(int ctl)
-{
-	char msg[1024];
-
-	/* wait for hello from other side */
-	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
-		werrstr("HELLO: %s", msg);
-		return Server;
-	}
-	return 0;
-}
-
-int
-getdec(char *p, int n)
-{
-	int x = 0;
-	int i;
-
-	for(i = 0; i < n; i++)
-		x = x*10 + (*p++ - '0');
-	return x;
-}
-
-int
-ftprestart(int ctl, Out *out, URL *u, Range *r, long mtime)
-{
-	Tm tm;
-	char msg[1024];
-	long x, rmtime;
-
-	ftpcmd(ctl, "MDTM %s", u->page);
-	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
-		r->start = 0;
-		return 0;		/* need to do something */
-	}
-
-	/* decode modification time */
-	if(strlen(msg) < 4 + 4 + 2 + 2 + 2 + 2 + 2){
-		r->start = 0;
-		return 0;		/* need to do something */
-	}
-	memset(&tm, 0, sizeof(tm));
-	tm.year = getdec(msg+4, 4) - 1900;
-	tm.mon = getdec(msg+4+4, 2) - 1;
-	tm.mday = getdec(msg+4+4+2, 2);
-	tm.hour = getdec(msg+4+4+2+2, 2);
-	tm.min = getdec(msg+4+4+2+2+2, 2);
-	tm.sec = getdec(msg+4+4+2+2+2+2, 2);
-	strcpy(tm.zone, "GMT");
-	rmtime = tm2sec(&tm);
-	if(rmtime > mtime)
-		r->start = 0;
-
-	/* get size */
-	ftpcmd(ctl, "SIZE %s", u->page);
-	if(ftprcode(ctl, msg, sizeof(msg)) == Success){
-		x = atol(msg+4);
-		if(r->start == x)
-			return 1;	/* we're up to date */
-		r->end = x;
-	}
-
-	/* seek to restart point */
-	if(r->start > 0){
-		ftpcmd(ctl, "REST %lud", r->start);
-		if(ftprcode(ctl, msg, sizeof(msg)) == Incomplete){
-			setoffset(out, r->start);
-		}else
-			r->start = 0;
-	}
-
-	return 0;	/* need to do something */
-}
-
-int
-logon(int ctl)
-{
-	char msg[1024];
-
-	/* login anonymous */
-	ftpcmd(ctl, "USER anonymous");
-	switch(ftprcode(ctl, msg, sizeof(msg))){
-	case Success:
-		return 0;
-	case Incomplete:
-		break;	/* need password */
-	default:
-		werrstr("USER: %s", msg);
-		return Server;
-	}
-
-	/* send user id as password */
-	sprint(msg, "%s@closedmind.org", getuser());
-	ftpcmd(ctl, "PASS %s", msg);
-	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
-		werrstr("PASS: %s", msg);
-		return Server;
-	}
-
-	return 0;
-}
-
-int
-xfertype(int ctl, char *t)
-{
-	char msg[1024];
-
-	ftpcmd(ctl, "TYPE %s", t);
-	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
-		werrstr("TYPE %s: %s", t, msg);
-		return Server;
-	}
-
-	return 0;
-}
-
-int
-passive(int ctl, URL *u)
-{
-	char msg[1024];
-	char ipaddr[32];
-	char *f[6];
-	char *p;
-	int fd;
-	int port;
-	char aport[12];
-
-	ftpcmd(ctl, "PASV");
-	if(ftprcode(ctl, msg, sizeof(msg)) != Success)
-		return Error;
-
-	/* get address and port number from reply, this is AI */
-	p = strchr(msg, '(');
-	if(p == nil){
-		for(p = msg+3; *p; p++)
-			if(isdigit(*p))
-				break;
-	} else
-		p++;
-	if(getfields(p, f, 6, 0, ",)") < 6){
-		werrstr("ftp protocol botch");
-		return Server;
-	}
-	snprint(ipaddr, sizeof(ipaddr), "%s.%s.%s.%s",
-		f[0], f[1], f[2], f[3]);
-	port = ((atoi(f[4])&0xff)<<8) + (atoi(f[5])&0xff);
-	sprint(aport, "%d", port);
-
-	/* open data connection */
-	fd = dial(netmkaddr(ipaddr, tcpdir, aport), 0, 0, 0);
-	if(fd < 0){
-		werrstr("passive mode failed: %r");
-		return Error;
-	}
-
-	/* tell remote to send a file */
-	ftpcmd(ctl, "RETR %s", u->page);
-	if(ftprcode(ctl, msg, sizeof(msg)) != Extra){
-		werrstr("RETR %s: %s", u->page, msg);
-		return Error;
-	}
-	return fd;
-}
-
-int
-active(int ctl, URL *u)
-{
-	char msg[1024];
-	char dir[40], ldir[40];
-	uchar ipaddr[4];
-	uchar port[2];
-	int lcfd, dfd, afd;
-
-	/* announce a port for the call back */
-	snprint(msg, sizeof(msg), "%s!*!0", tcpdir);
-	afd = announce(msg, dir);
-	if(afd < 0)
-		return Error;
-
-	/* get a local address/port of the annoucement */
-	if(getaddrport(dir, ipaddr, port) < 0){
-		close(afd);
-		return Error;
-	}
-
-	/* tell remote side address and port*/
-	ftpcmd(ctl, "PORT %d,%d,%d,%d,%d,%d", ipaddr[0], ipaddr[1], ipaddr[2],
-		ipaddr[3], port[0], port[1]);
-	if(ftprcode(ctl, msg, sizeof(msg)) != Success){
-		close(afd);
-		werrstr("active: %s", msg);
-		return Error;
-	}
-
-	/* tell remote to send a file */
-	ftpcmd(ctl, "RETR %s", u->page);
-	if(ftprcode(ctl, msg, sizeof(msg)) != Extra){
-		close(afd);
-		werrstr("RETR: %s", msg);
-		return Server;
-	}
-
-	/* wait for a connection */
-	lcfd = listen(dir, ldir);
-	if(lcfd < 0){
-		close(afd);
-		return Error;
-	}
-	dfd = accept(lcfd, ldir);
-	if(dfd < 0){
-		close(afd);
-		close(lcfd);
-		return Error;
-	}
-	close(afd);
-	close(lcfd);
-	
-	return dfd;
-}
-
-int
-ftpxfer(int in, Out *out, Range *r)
-{
-	char buf[1024];
-	long vtime;
-	int i, n;
-
-	vtime = 0;
-	for(n = 0;;n += i){
-		i = read(in, buf, sizeof(buf));
-		if(i == 0)
-			break;
-		if(i < 0)
-			return Error;
-		if(output(out, buf, i) != i)
-			return Error;
-		r->start += i;
-		if(verbose && (vtime != time(0) || r->start == r->end)) {
-			vtime = time(0);
-			fprint(2, "%ld %ld\n", r->start, r->end);
-		}
-	}
-	return n;
-}
-
-int
-terminateftp(int ctl, int rv)
-{
-	close(ctl);
-	return rv;
-}
-
-/*
- * case insensitive strcmp (why aren't these in libc?)
- */
-int
-cistrncmp(char *a, char *b, int n)
-{
-	while(n-- > 0){
-		if(tolower(*a++) != tolower(*b++))
-			return -1;
-	}
-	return 0;
-}
-
-int
-cistrcmp(char *a, char *b)
-{
-	while(*a || *b)
-		if(tolower(*a++) != tolower(*b++))
-			return -1;
-
-	return 0;
-}
-
-/*
- *  buffered io
- */
-struct
-{
-	char *rp;
-	char *wp;
-	char buf[4*1024];
-} b;
-
-void
-initibuf(void)
-{
-	b.rp = b.wp = b.buf;
-}
-
-/*
- *  read a possibly buffered line, strip off trailing while
- */
-int
-readline(int fd, char *buf, int len)
-{
-	int n;
-	char *p;
-	int eof = 0;
-
-	len--;
-
-	for(p = buf;;){
-		if(b.rp >= b.wp){
-			n = read(fd, b.wp, sizeof(b.buf)/2);
-			if(n < 0)
-				return -1;
-			if(n == 0){
-				eof = 1;
-				break;
-			}
-			b.wp += n;
-		}
-		n = *b.rp++;
-		if(len > 0){
-			*p++ = n;
-			len--;
-		}
-		if(n == '\n')
-			break;
-	}
-
-	/* drop trailing white */
-	for(;;){
-		if(p <= buf)
-			break;
-		n = *(p-1);
-		if(n != ' ' && n != '\t' && n != '\r' && n != '\n')
-			break;
-		p--;
-	}
-	*p = 0;
-
-	if(eof && p == buf)
-		return -1;
-
-	return p-buf;
-}
-
-void
-unreadline(char *line)
-{
-	int i, n;
-
-	i = strlen(line);
-	n = b.wp-b.rp;
-	memmove(&b.buf[i+1], b.rp, n);
-	memmove(b.buf, line, i);
-	b.buf[i] = '\n';
-	b.rp = b.buf;
-	b.wp = b.rp + i + 1 + n;
-}
-
-int
-readibuf(int fd, char *buf, int len)
-{
-	int n;
-
-	n = b.wp-b.rp;
-	if(n > 0){
-		if(n > len)
-			n = len;
-		memmove(buf, b.rp, n);
-		b.rp += n;
-		return n;
-	}
-	return read(fd, buf, len);
-}
-
-int
-dfprint(int fd, char *fmt, ...)
-{
-	char buf[4*1024];
-	va_list arg;
-
-	va_start(arg, fmt);
-	vseprint(buf, buf+sizeof(buf), fmt, arg);
-	va_end(arg);
-	if(debug)
-		fprint(2, "%d -> %s", fd, buf);
-	return fprint(fd, "%s", buf);
-}
-
-int
-getaddrport(char *dir, uchar *ipaddr, uchar *port)
-{
-	char buf[256];
-	int fd, i;
-	char *p;
-
-	snprint(buf, sizeof(buf), "%s/local", dir);
-	fd = open(buf, OREAD);
-	if(fd < 0)
-		return -1;
-	i = read(fd, buf, sizeof(buf)-1);
-	close(fd);
-	if(i <= 0)
-		return -1;
-	buf[i] = 0;
-	p = strchr(buf, '!');
-	if(p != nil)
-		*p++ = 0;
-	v4parseip(ipaddr, buf);
-	i = atoi(p);
-	port[0] = i>>8;
-	port[1] = i;
-	return 0;
-}
-
-void
-md5free(DigestState *state)
-{
-	uchar x[MD5dlen];
-	md5(nil, 0, x, state);
-}
-
-DigestState*
-md5dup(DigestState *state)
-{
-	char *p;
-
-	p = md5pickle(state);
-	if(p == nil)
-		sysfatal("md5pickle: %r");
-	state = md5unpickle(p);
-	if(state == nil)
-		sysfatal("md5unpickle: %r");
-	free(p);
-	return state;
-}
-
-void
-setoffset(Out *out, int offset)
-{
-	md5free(out->curr);
-	if(offset == 0)
-		out->curr = md5(nil, 0, nil, nil);
-	else
-		out->curr = nil;
-	out->offset = offset;
-	out->written = offset;
-	if(ofile != nil)
-		if(seek(out->fd, offset, 0) != offset)
-			sysfatal("seek: %r");
-}
-
-/*
- * write some output, discarding it (but keeping track)
- * if we've already written it. if we've gone backwards,
- * verify that everything previously written matches
- * that which would have been written from the current
- * output.
- */
-int
-output(Out *out, char *buf, int nb)
-{
-	int n, d;
-	uchar m0[MD5dlen], m1[MD5dlen];
-
-	n = nb;
-	d = out->written - out->offset;
-	assert(d >= 0);
-	if(d > 0){
-		if(n < d){
-			if(out->curr != nil)
-				md5((uchar*)buf, n, nil, out->curr);
-			out->offset += n;
-			return n;
-		}
-		if(out->curr != nil){
-			md5((uchar*)buf, d, m0, out->curr);
-			out->curr = nil;
-			md5(nil, 0, m1, md5dup(out->hiwat));
-			if(memcmp(m0, m1, MD5dlen) != 0){
-				fprint(2, "integrity check failure at offset %d\n", out->written);
-				return -1;
-			}
-		}
-		buf += d;
-		n -= d;
-		out->offset += d;
-	}
-	if(n > 0){
-		out->hiwat = md5((uchar*)buf, n, nil, out->hiwat);
-		n = write(out->fd, buf, n);
-		if(n > 0){
-			out->offset += n;
-			out->written += n;
-		}
-	}
-	return n + d;
-}
-
--- a/sys/src/cmd/webfs/buf.c
+++ /dev/null
@@ -1,89 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <ip.h>
-#include <plumb.h>
-#include <thread.h>
-#include <fcall.h>
-#include <9p.h>
-#include "dat.h"
-#include "fns.h"
-
-void
-initibuf(Ibuf *b, Ioproc *io, int fd)
-{
-	b->fd = fd;
-	b->io = io;
-	b->rp = b->wp = b->buf;
-}
-
-int
-readibuf(Ibuf *b, char *buf, int len)
-{
-	int n;
-
-	n = b->wp - b->rp;
-	if(n > 0){
-		if(n > len)
-			n = len;
-		memmove(buf, b->rp, n);
-		b->rp += n;
-		return n;
-	}
-	return ioreadn(b->io, b->fd, buf, len);
-}
-
-void
-unreadline(Ibuf *b, char *line)
-{
-	int i, n;
-
-	i = strlen(line);
-	n = b->wp - b->rp;
-	memmove(&b->buf[i+1], b->rp, n);
-	memmove(b->buf, line, i);
-	b->buf[i] = '\n';
-	b->rp = b->buf;
-	b->wp = b->rp+i+1+n;
-}
-
-int
-readline(Ibuf *b, char *buf, int len)
-{
-	int n;
-	char *p;
-
-	len--;
-
-	for(p = buf;;){
-		if(b->rp >= b->wp){
-			n = ioread(b->io, b->fd, b->wp, sizeof(b->buf)/2);
-			if(n < 0)
-				return -1;
-			if(n == 0)
-				break;
-			b->wp += n;
-		}
-		n = *b->rp++;
-		if(len > 0){
-			*p++ = n;
-			len--;
-		}
-		if(n == '\n')
-			break;
-	}
-
-	/* drop trailing white */
-	for(;;){
-		if(p <= buf)
-			break;
-		n = *(p-1);
-		if(n != ' ' && n != '\t' && n != '\r' && n != '\n')
-			break;
-		p--;
-	}
-
-	*p = 0;
-	return p-buf;
-}
-
--- /dev/null
+++ b/sys/src/cmd/webfs/buq.c
@@ -1,0 +1,263 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "dat.h"
+#include "fns.h"
+
+static void
+matchreq(Buq *q)
+{
+	Req *r;
+	Buf *b;
+	int l;
+
+	while(r = q->rh){
+		if((b = q->bh) == nil){
+			if(q->closed){
+				if((q->rh = r->aux) == nil)
+					q->rt = &q->rh;
+				if(r->ifcall.type == Tread)
+					r->ofcall.count = 0;
+				respond(r, q->error);
+				continue;
+			}
+			break;
+		}
+		if((q->rh = r->aux) == nil)
+			q->rt = &q->rh;
+		if(r->ifcall.type == Topen){
+			respond(r, nil);
+			continue;
+		}
+		l = b->ep - b->rp;
+		if(l > r->ifcall.count)
+			l = r->ifcall.count;
+		memmove(r->ofcall.data, b->rp, l);
+		r->ofcall.count = l;
+		respond(r, nil);
+		b->rp += l;
+		q->size -= l;
+		if(b->rp >= b->ep){
+			if((q->bh = b->next) == nil)
+				q->bt = &q->bh;
+			if(r = b->wreq){
+				r->ofcall.count = r->ifcall.count;
+				respond(r, nil);
+			}
+			free(b);
+		}
+	}
+	rwakeupall(&q->rz);
+}
+
+int
+buread(Buq *q, void *v, int l)
+{
+	Req *r;
+	Buf *b;
+
+	qlock(q);
+	while((b = q->bh) == nil){
+		if(q->closed){
+			l = 0;
+			if(q->error){
+				werrstr("%s", q->error);
+				l = -1;
+			}
+			qunlock(q);
+			return l;
+		}
+		rsleep(&q->rz);
+	}
+	if(l > (b->ep - b->rp))
+		l = b->ep - b->rp;
+	memmove(v, b->rp, l);
+	b->rp += l;
+	q->size -= l;
+	rwakeup(&q->rz);
+	if(b->rp < b->ep){
+		qunlock(q);
+		return l;
+	}
+	if((q->bh = b->next) == nil)
+		q->bt = &q->bh;
+	qunlock(q);
+	if(r = b->wreq){
+		r->ofcall.count = r->ifcall.count;
+		respond(r, nil);
+	}
+	free(b);
+	return l;
+}
+
+int
+buwrite(Buq *q, void *v, int l)
+{
+	Buf *b;
+
+	b = emalloc(sizeof(*b) + l);
+	b->wreq = nil;
+	b->rp = b->end;
+	b->ep = b->rp + l;
+	memmove(b->rp, v, l);
+	b->next = nil;
+	qlock(q);
+	if(q->closed){
+		l = 0;
+		if(q->error){
+			werrstr("%s", q->error);
+			l = -1;
+		}
+		qunlock(q);
+		free(b);
+		return l;
+	}
+	*q->bt = b;
+	q->bt = &b->next;
+	q->size += l;
+	matchreq(q);
+	while(!q->closed && q->size >= q->limit)
+		rsleep(&q->rz);
+	qunlock(q);
+	return l;
+}
+
+void
+buclose(Buq *q, char *error)
+{
+	if(q == nil)
+		return;
+	qlock(q);
+	if(!q->closed){
+		if(error)
+			q->error = estrdup9p(error);
+		q->closed = 1;
+		matchreq(q);
+	}
+	qunlock(q);
+}
+
+Buq*
+bualloc(int limit)
+{
+	Buq *q;
+
+	q = emalloc(sizeof(*q));
+	q->limit = limit;
+	q->rt = &q->rh;
+	q->bt = &q->bh;
+	q->rz.l = q;
+	incref(q);
+	return q;
+}
+
+void
+bufree(Buq *q)
+{
+	Buf *b;
+	Key *k;
+
+	if(q == nil || decref(q))
+		return;
+	while(b = q->bh){
+		q->bh = b->next;
+		free(b);
+	}
+	freeurl(q->url);
+	while(k = q->hdr){
+		q->hdr = k->next;
+		free(k);
+	}
+	free(q->error);
+	free(q);
+}
+
+void
+bureq(Buq *q, Req *r)
+{
+	Buf *b;
+	int l;
+
+	switch(r->ifcall.type){
+	default:
+		respond(r, "bug in bureq");
+		return;
+	case Twrite:
+		l = r->ifcall.count;
+		if((q->size + l) < q->limit){
+			r->ofcall.count = buwrite(q, r->ifcall.data, r->ifcall.count);
+			respond(r, nil);
+			return;
+		}
+		b = emalloc(sizeof(*b));
+		b->wreq = r;
+		b->rp = (uchar*)r->ifcall.data;
+		b->ep = b->rp + l;
+		b->next = nil;
+		qlock(q);
+		*q->bt = b;
+		q->bt = &b->next;
+		q->size += l;
+		break;
+	case Tread:
+	case Topen:
+		r->aux = nil;
+		qlock(q);
+		*q->rt = r;
+		q->rt = (Req**)&r->aux;
+		break;
+	}
+	matchreq(q);
+	qunlock(q);
+}
+
+void
+buflushreq(Buq *q, Req *r)
+{
+	Buf **bb, *b;
+	Req **rr;
+	int l;
+
+	switch(r->ifcall.type){
+	default:
+		respond(r, "bug in bufflushreq");
+		return;
+	case Twrite:
+		qlock(q);
+		for(bb = &q->bh; b = *bb; bb = &b->next){
+			if(b->wreq != r)
+				continue;
+			/* fake successfull write */
+			l = b->ep - b->rp;
+			b = realloc(b, sizeof(*b) + l);
+			memmove(b->end, b->rp, l);
+			b->rp = b->end;
+			b->ep = b->rp + l;
+			b->wreq = nil;
+			*bb = b;
+			if(b->next == nil)
+				q->bt = &b->next;
+			r->ofcall.count = r->ifcall.count;
+			respond(r, nil);
+			break;
+		}
+		break;
+	case Topen:
+	case Tread:
+		qlock(q);
+		for(rr = &q->rh; *rr; rr = (Req**)&((*rr)->aux)){
+			if(*rr != r)
+				continue;
+			if((*rr = r->aux) == nil)
+				q->rt = rr;
+			respond(r, "interrupted");
+			break;
+		}
+		break;
+	}
+	qunlock(q);
+}
--- a/sys/src/cmd/webfs/client.c
+++ /dev/null
@@ -1,411 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <ip.h>
-#include <plumb.h>
-#include <thread.h>
-#include <fcall.h>
-#include <9p.h>
-#include "dat.h"
-#include "fns.h"
-
-int nclient;
-Client **client;
-
-static void clientthread(void*);
-int
-newclient(int plumbed)
-{
-	int i;
-	Client *c;
-
-	for(i=0; i<nclient; i++)
-		if(client[i]->ref==0)
-			return i;
-
-	c = emalloc(sizeof(Client));
-	c->plumbed = plumbed;
-	c->creq = chancreate(sizeof(Req*), 8);
-	threadcreate(clientthread, c, STACK);
-
-	c->io = ioproc();
-	c->num = nclient;
-	c->ctl = globalctl;
-	clonectl(&c->ctl);
-	if(nclient%16 == 0)
-		client = erealloc(client, (nclient+16)*sizeof(client[0]));
-	client[nclient++] = c;
-	return nclient-1;
-}
-
-void
-closeclient(Client *c)
-{
-	if(--c->ref == 0){
-		if(c->bodyopened){
-			if(c->url && c->url->close)
-				(*c->url->close)(c);
-			c->bodyopened = 0;
-		}
-		free(c->contenttype);
-		c->contenttype = nil;
-		free(c->postbody);
-		c->postbody = nil;
-		freeurl(c->url);
-		c->url = nil;
-		freeurl(c->baseurl);
-		c->baseurl = nil;
-		free(c->redirect);
-		c->redirect = nil;
-		free(c->authenticate);
-		c->authenticate = nil;
-		c->npostbody = 0;
-		c->havepostbody = 0;
-		c->bodyopened = 0;
-	}
-}
-
-void
-clonectl(Ctl *c)
-{
-	if(c->useragent)
-		c->useragent = estrdup(c->useragent);
-}
-
-void
-clientbodyopen(Client *c, Req *r)
-{
-	char e[ERRMAX], *next, *frag;
-	int i, nauth;
-	Url *u;
-
-	nauth = 0;
-	next = nil;
-	for(i=0; i<=c->ctl.redirectlimit; i++){
-		if(c->url == nil){
-			werrstr("nil url");
-			goto Error;
-		}
-		if(c->url->open == nil){
-			werrstr("unsupported url type");
-			goto Error;
-		}
-		if(fsdebug)
-			fprint(2, "try %s\n", c->url->url);
-		if(c->url->open(c, c->url) < 0){
-		Error:
-			rerrstr(e, sizeof e);
-			if(next)
-				fprint(2, "next %s (but for error)\n", next);
-			free(next);
-			c->iobusy = 0;
-			if(r != nil)
-				r->fid->omode = -1;
-			closeclient(c);	/* not opening */
-			if(r != nil)
-				respond(r, e);
-			return;
-		}
-		free(next);
-		next = c->redirect;
-		c->redirect = nil;
-		if(c->authenticate && nauth++ < 1){
-			if(c->url->close)
-				(*c->url->close)(c);
-			continue;
-		}
-		if(next == nil)
-			break;
-		if(i==c->ctl.redirectlimit){
-			werrstr("redirect limit reached");
-			goto Error;
-		}
-		if((u = parseurl(next, c->url)) == nil)
-			goto Error;
-		/* if there was a redirect, carry over the fragment */
-		if((frag = c->url->fragment) && u->fragment == nil){
-			u->fragment = estrdup(frag);
-			rewriteurl(u);
-		}
-		if(urldebug)
-			fprint(2, "parseurl %s got scheme %d\n", next, u->ischeme);
-		if(u->ischeme == USunknown){
-			werrstr("redirect with unknown URL scheme");
-			goto Error;
-		}
-		if(u->ischeme == UScurrent){
-			werrstr("redirect to URL relative to current document");
-			goto Error;
-		}
-		if(c->url->close)
-			(*c->url->close)(c);
-		freeurl(c->url);
-		c->url = u;
-	}
-	free(next);
-	c->iobusy = 0;
-	if(r != nil)
-		respond(r, nil);
-}
-
-void
-plumburl(char *url, char *base)
-{
-	int i;
-	Client *c;
-	Url *ubase, *uurl;
-
-	ubase = nil;
-	if(base){
-		ubase = parseurl(base, nil);
-		if(ubase == nil)
-			return;
-	}
-	uurl = parseurl(url, ubase);
-	if(uurl == nil){
-		freeurl(ubase);
-		return;
-	}
-	i = newclient(1);
-	c = client[i];
-	c->ref++;
-	c->baseurl = ubase;
-	c->url = uurl;
-	sendp(c->creq, nil);
-}
-
-void
-clientbodyread(Client *c, Req *r)
-{
-	char e[ERRMAX];
-
-	if(c->url->read == nil){
-		respond(r, "unsupported url type");
-		return;
-	}
-	if(c->url->read(c, r) < 0){
-		rerrstr(e, sizeof e);
-		c->iobusy = 0;
-		respond(r, e);
-		return;
-	}
-	c->iobusy = 0;
-	respond(r, nil);
-}
-
-static void
-clientthread(void *a)
-{
-	Client *c;
-	Req *r;
-
-	c = a;
-	if(c->plumbed) {
-		recvp(c->creq);
-		if(c->url == nil){
-			fprint(2, "bad url got plumbed\n");
-			return;
-		}
-		clientbodyopen(c, nil);
-		replumb(c);
-	}
-	while((r = recvp(c->creq)) != nil){
-		if(fsdebug)
-			fprint(2, "clientthread %F\n", &r->ifcall);
-		switch(r->ifcall.type){
-		case Topen:
-			if(c->plumbed) {
-				c->plumbed = 0;
-				c->ref--;			/* from plumburl() */
-				respond(r, nil);
-			}
-			else
-				clientbodyopen(c, r);
-			break;
-		case Tread:
-			clientbodyread(c, r);
-			break;
-		case Tflush:
-			respond(r, nil);
-		}
-		if(fsdebug)
-			fprint(2, "clientthread finished req\n");
-	}
-}
-	
-enum
-{
-	Bool,
-	Int,
-	String,
-	XRel,
-	XUrl,
-	Fn,
-};
-
-typedef struct Ctab Ctab;
-struct Ctab {
-	char *name;
-	int type;
-	void *offset;
-};
-
-Ctab ctltab[] = {
-	"acceptcookies",	Bool,		(void*)offsetof(Ctl, acceptcookies),
-	"sendcookies",		Bool,		(void*)offsetof(Ctl, sendcookies),
-	"redirectlimit",		Int,		(void*)offsetof(Ctl, redirectlimit),
-	"useragent",		String,	(void*)offsetof(Ctl, useragent),
-};
-
-Ctab globaltab[] = {
-	"chatty9p",		Int,		&chatty9p,
-	"fsdebug",		Int,		&fsdebug,
-	"cookiedebug",		Int,		&cookiedebug,
-	"urldebug",		Int,		&urldebug,
-	"httpdebug",		Int,		&httpdebug,
-};
-
-Ctab clienttab[] = {
-	"baseurl",			XUrl,		(void*)offsetof(Client, baseurl),
-	"url",				XRel,		(void*)offsetof(Client, url),
-};
-
-static Ctab*
-findcmd(char *cmd, Ctab *tab, int ntab)
-{
-	int i;
-
-	for(i=0; i<ntab; i++)
-		if(strcmp(tab[i].name, cmd) == 0)
-			return &tab[i];
-	return nil;
-}
-
-static void
-parseas(Req *r, char *arg, int type, void *a)
-{
-	Url *u, *base;
-	char e[ERRMAX];
-
-	base = nil;
-	switch(type){
-	case Bool:
-		if(strcmp(arg, "on")==0 || strcmp(arg, "1")==0)
-			*(int*)a = 1;
-		else
-			*(int*)a = 0;
-		break;
-	case String:
-		free(*(char**)a);
-		*(char**)a = estrdup(arg);
-		break;
-	case XRel:
-		base = ((Client*)a)->baseurl;
-	case XUrl:
-		u = parseurl(arg, base);
-		if(u == nil){
-			snprint(e, sizeof e, "parseurl: %r");
-			respond(r, e);
-			return;
-		}
-		freeurl(*(Url**)a);
-		*(Url**)a = u;
-		break;
-	case Int:
-		if(strcmp(arg, "on")==0)
-			*(int*)a = 1;
-		else
-			*(int*)a = atoi(arg);
-		break;
-	}
-	respond(r, nil);
-}
-
-int
-ctlwrite(Req *r, Ctl *ctl, char *cmd, char *arg)
-{
-	void *a;
-	Ctab *t;
-
-	if((t = findcmd(cmd, ctltab, nelem(ctltab))) == nil)
-		return 0;
-	a = (void*)((uintptr)ctl+(uintptr)t->offset);
-	parseas(r, arg, t->type, a);
-	return 1;
-}
-
-int
-clientctlwrite(Req *r, Client *c, char *cmd, char *arg)
-{
-	void *a;
-	Ctab *t;
-
-	if((t = findcmd(cmd, clienttab, nelem(clienttab))) == nil)
-		return 0;
-	a = (void*)((uintptr)c+(uintptr)t->offset);
-	parseas(r, arg, t->type, a);
-	return 1;
-}
-
-int
-globalctlwrite(Req *r, char *cmd, char *arg)
-{
-	void *a;
-	Ctab *t;
-
-	if((t = findcmd(cmd, globaltab, nelem(globaltab))) == nil)
-		return 0;
-	a = t->offset;
-	parseas(r, arg, t->type, a);
-	return 1;
-}
-
-static void
-ctlfmt(Ctl *c, char *s)
-{
-	int i;
-	void *a;
-	char *t;
-
-	for(i=0; i<nelem(ctltab); i++){
-		a = (void*)((uintptr)c+(uintptr)ctltab[i].offset);
-		switch(ctltab[i].type){
-		case Bool:
-			s += sprint(s, "%s %s\n", ctltab[i].name, *(int*)a ? "on" : "off");
-			break;
-		case Int:
-			s += sprint(s, "%s %d\n", ctltab[i].name, *(int*)a);
-			break;
-		case String:
-			t = *(char**)a;
-			if(t != nil)
-				s += sprint(s, "%s %.*s%s\n", ctltab[i].name, utfnlen(t, 100), t, strlen(t)>100 ? "..." : "");
-			break;
-		}
-	}
-}
-
-void
-ctlread(Req *r, Client *c)
-{
-	char buf[1024];
-
-	sprint(buf, "%11d \n", c->num);
-	ctlfmt(&c->ctl, buf+strlen(buf));
-	readstr(r, buf);
-	respond(r, nil);
-}
-
-void
-globalctlread(Req *r)
-{
-	char buf[1024], *s;
-	int i;
-
-	s = buf;
-	for(i=0; i<nelem(globaltab); i++)
-		s += sprint(s, "%s %d\n", globaltab[i].name, *(int*)globaltab[i].offset);
-	ctlfmt(&globalctl, s);
-	readstr(r, buf);
-	respond(r, nil);
-}
--- a/sys/src/cmd/webfs/cookies.c
+++ /dev/null
@@ -1,1179 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <ndb.h>
-#include <fcall.h>
-#include <thread.h>
-#include <9p.h>
-#include <ctype.h>
-#include "dat.h"
-#include "fns.h"
-
-int cookiedebug;
-
-typedef struct Cookie Cookie;
-typedef struct Jar Jar;
-
-struct Cookie
-{
-	/* external info */
-	char*	name;
-	char*	value;
-	char*	dom;		/* starts with . */
-	char*	path;
-	char*	version;
-	char*	comment;		/* optional, may be nil */
-
-	uint		expire;		/* time of expiration: ~0 means when webcookies dies */
-	int		secure;
-	int		explicitdom;	/* dom was explicitly set */
-	int		explicitpath;	/* path was explicitly set */
-	int		netscapestyle;
-
-	/* internal info */
-	int		deleted;
-	int		mark;
-	int		ondisk;
-};
-
-struct Jar
-{
-	Cookie	*c;
-	int		nc;
-	int		mc;
-
-	Qid		qid;
-	int		dirty;
-	char		*file;
-	char		*lockfile;
-};
-
-struct {
-	char *s;
-	int	offset;
-	int	ishttp;
-} stab[] = {
-	"domain",		offsetof(Cookie, dom),		1,
-	"path",		offsetof(Cookie, path),		1,
-	"name",		offsetof(Cookie, name),		0,
-	"value",		offsetof(Cookie, value),		0,
-	"comment",	offsetof(Cookie, comment),	1,
-	"version",		offsetof(Cookie, version),		1,
-};
-
-struct {
-	char *s;
-	int	offset;
-} itab[] = {
-	"expire",			offsetof(Cookie, expire),
-	"secure",			offsetof(Cookie, secure),
-	"explicitdomain",	offsetof(Cookie, explicitdom),
-	"explicitpath",		offsetof(Cookie, explicitpath),
-	"netscapestyle",	offsetof(Cookie, netscapestyle),
-};
-
-#pragma varargck type "J"	Jar*
-#pragma varargck type "K"	Cookie*
-
-/* HTTP format */
-static int
-jarfmt(Fmt *fp)
-{
-	int i;
-	Jar *jar;
-
-	jar = va_arg(fp->args, Jar*);
-
-	if(jar == nil || jar->nc == 0)
-		return 0;
-
-	fmtstrcpy(fp, "Cookie: ");
-	if(jar->c[0].version)
-		fmtprint(fp, "$Version=%s; ", jar->c[0].version);
-	for(i=0; i<jar->nc; i++)
-		fmtprint(fp, "%s%s=%s", i ? "; ": "", jar->c[i].name, jar->c[i].value);
-	fmtstrcpy(fp, "\r\n");
-	return 0;
-}
-
-/* individual cookie */
-static int
-cookiefmt(Fmt *fp)
-{
-	int j, k, first;
-	char *t;
-	Cookie *c;
-
-	c = va_arg(fp->args, Cookie*);
-
-	first = 1;
-	for(j=0; j<nelem(stab); j++){
-		t = *(char**)((uintptr)c+stab[j].offset);
-		if(t == nil)
-			continue;
-		if(first)
-			first = 0;
-		else
-			fmtstrcpy(fp, " ");
-		fmtprint(fp, "%s=%q", stab[j].s, t);
-	}
-	for(j=0; j<nelem(itab); j++){
-		k = *(int*)((uintptr)c+itab[j].offset);
-		if(k == 0)
-			continue;
-		if(first)
-			first = 0;
-		else
-			fmtstrcpy(fp, " ");
-		fmtprint(fp, "%s=%ud", itab[j].s, k);
-	}
-	return 0;
-}
-
-/*
- * sort cookies:
- *	- alpha by name
- *	- alpha by domain
- *	- longer paths first, then alpha by path (RFC2109 4.3.4)
- */
-static int
-cookiecmp(Cookie *a, Cookie *b)
-{
-	int i;
-
-	if((i = strcmp(a->name, b->name)) != 0)
-		return i;
-	if((i = cistrcmp(a->dom, b->dom)) != 0)
-		return i;
-	if((i = strlen(b->path) - strlen(a->path)) != 0)
-		return i;
-	if((i = strcmp(a->path, b->path)) != 0)
-		return i;
-	return 0;
-}
-
-static int
-exactcookiecmp(Cookie *a, Cookie *b)
-{
-	int i;
-
-	if((i = cookiecmp(a, b)) != 0)
-		return i;
-	if((i = strcmp(a->value, b->value)) != 0)
-		return i;
-	if(a->version || b->version){
-		if(!a->version)
-			return -1;
-		if(!b->version)
-			return 1;
-		if((i = strcmp(a->version, b->version)) != 0)
-			return i;
-	}
-	if(a->comment || b->comment){
-		if(!a->comment)
-			return -1;
-		if(!b->comment)
-			return 1;
-		if((i = strcmp(a->comment, b->comment)) != 0)
-			return i;
-	}
-	if((i = b->expire - a->expire) != 0)
-		return i;
-	if((i = b->secure - a->secure) != 0)
-		return i;
-	if((i = b->explicitdom - a->explicitdom) != 0)
-		return i;
-	if((i = b->explicitpath - a->explicitpath) != 0)
-		return i;
-	if((i = b->netscapestyle - a->netscapestyle) != 0)
-		return i;
-
-	return 0;
-}
-
-static void
-freecookie(Cookie *c)
-{
-	int i;
-
-	for(i=0; i<nelem(stab); i++)
-		free(*(char**)((uintptr)c+stab[i].offset));
-}
-
-static void
-copycookie(Cookie *c)
-{
-	int i;
-	char **ps;
-
-	for(i=0; i<nelem(stab); i++){
-		ps = (char**)((uintptr)c+stab[i].offset);
-		if(*ps)
-			*ps = estrdup(*ps);
-	}
-}
-
-static void
-delcookie(Jar *j, Cookie *c)
-{
-	int i;
-
-	j->dirty = 1;
-	i = c - j->c;
-	if(i < 0 || i >= j->nc)
-		abort();
-	c->deleted = 1;
-}
-
-static void
-addcookie(Jar *j, Cookie *c)
-{
-	int i;
-
-	if(!c->name || !c->value || !c->path || !c->dom){
-		fprint(2, "not adding incomplete cookie\n");
-		return;
-	}
-
-	if(cookiedebug)
-		fprint(2, "add %K\n", c);
-
-	for(i=0; i<j->nc; i++)
-		if(cookiecmp(&j->c[i], c) == 0){
-			if(cookiedebug)
-				fprint(2, "cookie %K matches %K\n", &j->c[i], c);
-			if(exactcookiecmp(&j->c[i], c) == 0){
-				if(cookiedebug)
-					fprint(2, "exactly!\n");
-				j->c[i].mark = 0;
-				return;
-			}
-			delcookie(j, &j->c[i]);
-		}
-
-	j->dirty = 1;
-	if(j->nc == j->mc){
-		j->mc += 16;
-		j->c = erealloc9p(j->c, j->mc*sizeof(Cookie));
-	}
-	j->c[j->nc] = *c;
-	copycookie(&j->c[j->nc]);
-	j->nc++;
-}
-
-static void
-purgejar(Jar *j)
-{
-	int i;
-
-	for(i=j->nc-1; i>=0; i--){
-		if(!j->c[i].deleted)
-			continue;
-		freecookie(&j->c[i]);
-		--j->nc;
-		j->c[i] = j->c[j->nc];
-	}
-}
-
-static void
-addtojar(Jar *jar, char *line, int ondisk)
-{
-	Cookie c;
-	int i, j, nf, *pint;
-	char *f[20], *attr, *val, **pstr;
-	
-	memset(&c, 0, sizeof c);
-	c.expire = ~0;
-	c.ondisk = ondisk;
-	nf = tokenize(line, f, nelem(f));
-	for(i=0; i<nf; i++){
-		attr = f[i];
-		if((val = strchr(attr, '=')) != nil)
-			*val++ = '\0';
-		else
-			val = "";
-		/* string attributes */
-		for(j=0; j<nelem(stab); j++){
-			if(strcmp(stab[j].s, attr) == 0){
-				pstr = (char**)((uintptr)&c+stab[j].offset);
-				*pstr = val;
-			}
-		}
-		/* integer attributes */
-		for(j=0; j<nelem(itab); j++){
-			if(strcmp(itab[j].s, attr) == 0){
-				pint = (int*)((uintptr)&c+itab[j].offset);
-				if(val[0]=='\0')
-					*pint = 1;
-				else
-					*pint = strtoul(val, 0, 0);
-			}
-		}
-	}
-	if(c.name==nil || c.value==nil || c.dom==nil || c.path==nil){
-		if(cookiedebug)
-			fprint(2, "ignoring fractional cookie %K\n", &c);
-		return;
-	}
-	addcookie(jar, &c);
-}
-
-static Jar*
-newjar(void)
-{
-	Jar *jar;
-
-	jar = emalloc(sizeof(Jar));
-	return jar;
-}
-
-static int
-expirejar(Jar *jar, int exiting)
-{
-	int i, n;
-	uint now;
-
-	now = time(0);
-	n = 0;
-	for(i=0; i<jar->nc; i++){
-		if(jar->c[i].expire < now || (exiting && jar->c[i].expire==~0)){
-			if(cookiedebug)
-				fprint(2, "cookie %K expired\n", &jar->c[i]);
-			delcookie(jar, &jar->c[i]);
-			n++;
-		}
-	}
-	return n;
-}
-
-static void
-dumpjar(Jar *jar, char *desc)
-{
-	int i;
-	Biobuf *b;
-	char *s;
-
-	print("%s\n", desc);
-	print("\tin memory:\n");
-	
-	for(i=0; i<jar->nc; i++)
-		print("\t%K%s%s%s\n", &jar->c[i],
-			jar->c[i].ondisk ? " ondisk" : "",
-			jar->c[i].deleted ? " deleted" : "",
-			jar->c[i].mark ? " mark" : "");
-	print("\n\ton disk:\n");
-	if((b = Bopen(jar->file, OREAD)) == nil){
-		print("\tno file\n");
-	}else{
-		while((s = Brdstr(b, '\n', 1)) != nil){
-			print("\t%s\n", s);
-			free(s);
-		}
-		Bterm(b);
-	}
-	print("\n");
-}
-
-static int
-syncjar(Jar *jar)
-{
-	int i, fd;
-	char *line;
-	Dir *d;
-	Biobuf *b;
-	Qid q;
-
-	if(jar->file==nil)
-		return 0;
-
-	memset(&q, 0, sizeof q);
-	if((d = dirstat(jar->file)) != nil){
-		q = d->qid;
-		if(d->qid.path != jar->qid.path || d->qid.vers != jar->qid.vers)
-			jar->dirty = 1;
-		free(d);
-	}
-
-	if(jar->dirty == 0)
-		return 0;
-
-	fd = -1;
-	for(i=0; i<50; i++){
-		if((fd = create(jar->lockfile, OWRITE, DMEXCL|0666)) < 0){
-			sleep(100);
-			continue;
-		}
-		break;
-	}
-	if(fd < 0){
-		if(cookiedebug)
-			fprint(2, "open %s: %r", jar->lockfile);
-		werrstr("cannot acquire jar lock: %r");
-		return -1;
-	}
-
-	for(i=0; i<jar->nc; i++)	/* mark is cleared by addcookie */
-		jar->c[i].mark = jar->c[i].ondisk;
-
-	if((b = Bopen(jar->file, OREAD)) == nil){
-		if(cookiedebug)
-			fprint(2, "Bopen %s: %r", jar->file);
-		werrstr("cannot read cookie file %s: %r", jar->file);
-		close(fd);
-		return -1;
-	}
-	for(; (line = Brdstr(b, '\n', 1)) != nil; free(line)){
-		if(*line == '#')
-			continue;
-		addtojar(jar, line, 1);
-	}
-	Bterm(b);
-
-	for(i=0; i<jar->nc; i++)
-		if(jar->c[i].mark && jar->c[i].expire != ~0)
-			delcookie(jar, &jar->c[i]);
-
-	purgejar(jar);
-
-	b = Bopen(jar->file, OTRUNC|OWRITE);
-	if(b == nil){
-		if(cookiedebug)
-			fprint(2, "Bopen write %s: %r", jar->file);
-		close(fd);
-		return -1;
-	}
-	Bprint(b, "# webcookies cookie jar\n");
-	Bprint(b, "# comments and non-standard fields will be lost\n");
-	for(i=0; i<jar->nc; i++){
-		if(jar->c[i].expire == ~0)
-			continue;
-		Bprint(b, "%K\n", &jar->c[i]);
-		jar->c[i].ondisk = 1;
-	}
-	Bterm(b);
-
-	jar->dirty = 0;
-	close(fd);
-	if((d = dirstat(jar->file)) != nil){
-		jar->qid = d->qid;
-		free(d);
-	}
-	return 0;
-}
-
-static Jar*
-readjar(char *file)
-{
-	char *lock, *p;
-	Jar *jar;
-
-	jar = newjar();
-	lock = emalloc(strlen(file)+10);
-	strcpy(lock, file);
-	if((p = strrchr(lock, '/')) != nil)
-		p++;
-	else
-		p = lock;
-	memmove(p+2, p, strlen(p)+1);
-	p[0] = 'L';
-	p[1] = '.';
-	jar->lockfile = lock;
-	jar->file = file;
-	jar->dirty = 1;
-
-	if(syncjar(jar) < 0){
-		free(jar->file);
-		free(jar->lockfile);
-		free(jar);
-		return nil;
-	}
-	return jar;
-}
-
-static void
-closejar(Jar *jar)
-{
-	int i;
-
-	if(jar == nil)
-		return;
-	expirejar(jar, 0);
-
-	if(syncjar(jar) < 0)
-		fprint(2, "warning: cannot rewrite cookie jar: %r\n");
-
-	for(i=0; i<jar->nc; i++)
-		freecookie(&jar->c[i]);
-
-	free(jar->file);
-	free(jar->c);
-	free(jar);	
-}
-
-/*
- * Domain name matching is per RFC2109, section 2:
- *
- * Hosts names can be specified either as an IP address or a FQHN
- * string.  Sometimes we compare one host name with another.  Host A's
- * name domain-matches host B's if
- *
- * * both host names are IP addresses and their host name strings match
- *   exactly; or
- *
- * * both host names are FQDN strings and their host name strings match
- *   exactly; or
- *
- * * A is a FQDN string and has the form NB, where N is a non-empty name
- *   string, B has the form .B', and B' is a FQDN string.  (So, x.y.com
- *   domain-matches .y.com but not y.com.)
- *
- * Note that domain-match is not a commutative operation: a.b.c.com
- * domain-matches .c.com, but not the reverse.
- *
- * (This does not verify that IP addresses and FQDN's are well-formed.)
- */
-static int
-isdomainmatch(char *name, char *pattern)
-{
-	int lname, lpattern;
-
-	if(cistrcmp(name, pattern)==0)
-		return 1;
-
-	if(strcmp(ipattr(name), "dom")==0 && pattern[0]=='.'){
-		lname = strlen(name);
-		lpattern = strlen(pattern);
-		/* e.g., name: www.google.com && pattern: .google.com */
-		if(lname >= lpattern && cistrcmp(name+lname-lpattern, pattern)==0)
-			return 1;
-		/* e.g., name: google.com && pattern: .google.com */
-		if(lpattern > lname &&
-		    cistrcmp(pattern+lpattern-lname, name) == 0)
-			return 1;
-	}
-	return 0;
-}
-
-/*
- * RFC2109 4.3.4:
- *	- domain must match
- *	- path in cookie must be a prefix of request path
- *	- cookie must not have expired
- */
-static int
-iscookiematch(Cookie *c, char *dom, char *path, uint now)
-{
-	return isdomainmatch(dom, c->dom)
-		&& strncmp(c->path, path, strlen(c->path))==0
-		&& (c->expire == 0 || c->expire >= now);
-}
-
-/* 
- * Produce a subjar of matching cookies.
- * Secure cookies are only included if secure is set.
- */
-static Jar*
-cookiesearch(Jar *jar, char *dom, char *path, int issecure)
-{
-	int i;
-	Jar *j;
-	uint now;
-
-	if(cookiedebug)
-		fprint(2, "cookiesearch %s %s %d\n", dom, path, issecure);
-	now = time(0);
-	j = newjar();
-	for(i=0; i<jar->nc; i++){
-		if((issecure || !jar->c[i].secure) &&
-		    iscookiematch(&jar->c[i], dom, path, now)){
-			if(cookiedebug){
-				fprint(2, "\t%s %s %d %s\n", jar->c[i].dom,
-					jar->c[i].path, jar->c[i].secure,
-					jar->c[i].name);
-				fprint(2, "\tmatched\n");
-			}
-			addcookie(j, &jar->c[i]);
-		}
-	}
-	if(j->nc == 0){
-		closejar(j);
-		werrstr("no cookies found");
-		return nil;
-	}
-	qsort(j->c, j->nc, sizeof(j->c[0]), (int(*)(void*, void*))cookiecmp);
-	return j;
-}
-
-/*
- * RFC2109 4.3.2 security checks
- */
-static char*
-isbadcookie(Cookie *c, char *dom, char *path)
-{
-	int lcdom, ldom;
-
-	if(strncmp(c->path, path, strlen(c->path)) != 0)
-		return "cookie path is not a prefix of the request path";
-
-	/*
-	 * fgb says omitting this test is necessary to get some sites to work,
-	 * but it seems dubious.
-	 */
-	if(c->explicitdom && c->dom[0] != '.')
-		return "cookie domain doesn't start with dot";
-
-	lcdom = strlen(c->dom);
-	if(memchr(c->dom+1, '.', lcdom-1-1) == nil)
-		return "cookie domain doesn't have embedded dots";
-
-	if(!isdomainmatch(dom, c->dom))
-		return "request host does not match cookie domain";
-
-	ldom = strlen(dom);
-	if(strcmp(ipattr(dom), "dom")==0 && lcdom > ldom &&
-	    memchr(dom, '.', lcdom - ldom) != nil)
-		return "request host contains dots before cookie domain";
-
-	return 0;
-}
-
-/*
- * Sunday, 25-Jan-2002 12:24:36 GMT
- * Sunday, 25 Jan 2002 12:24:36 GMT
- * Sun, 25 Jan 02 12:24:36 GMT
- */
-static int
-isleap(int year)
-{
-	return year%4==0 && (year%100!=0 || year%400==0);
-}
-
-static uint
-strtotime(char *s)
-{
-	char *os;
-	int i;
-	Tm tm;
-
-	static int mday[2][12] = {
-		31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
-		31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31,
-	};
-	static char *wday[] = {
-		"Sunday", "Monday", "Tuesday", "Wednesday",
-		"Thursday", "Friday", "Saturday",
-	};
-	static char *mon[] = {
-		"Jan", "Feb", "Mar", "Apr", "May", "Jun",
-		"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
-	};
-
-	os = s;
-	/* Sunday, */
-	for(i=0; i<nelem(wday); i++){
-		if(cistrncmp(s, wday[i], strlen(wday[i])) == 0){
-			s += strlen(wday[i]);
-			break;
-		}
-		if(cistrncmp(s, wday[i], 3) == 0){
-			s += 3;
-			break;
-		}
-	}
-	if(i==nelem(wday)){
-		if(cookiedebug)
-			fprint(2, "bad wday (%s)\n", os);
-		return -1;
-	}
-	if(*s++ != ',' || *s++ != ' '){
-		if(cookiedebug)
-			fprint(2, "bad wday separator (%s)\n", os);
-		return -1;
-	}
-
-	/* 25- */
-	if(!isdigit(s[0]) || !isdigit(s[1]) || (s[2]!='-' && s[2]!=' ')){
-		if(cookiedebug)
-			fprint(2, "bad day of month (%s)\n", os);
-		return -1;
-	}
-	tm.mday = strtol(s, 0, 10);
-	s += 3;
-
-	/* Jan- */
-	for(i=0; i<nelem(mon); i++)
-		if(cistrncmp(s, mon[i], 3) == 0){
-			tm.mon = i;
-			s += 3;
-			break;
-		}
-	if(i==nelem(mon)){
-		if(cookiedebug)
-			fprint(2, "bad month (%s)\n", os);
-		return -1;
-	}
-	if(s[0] != '-' && s[0] != ' '){
-		if(cookiedebug)
-			fprint(2, "bad month separator (%s)\n", os);
-		return -1;
-	}
-	s++;
-
-	/* 2002 */
-	if(!isdigit(s[0]) || !isdigit(s[1])){
-		if(cookiedebug)
-			fprint(2, "bad year (%s)\n", os);
-		return -1;
-	}
-	tm.year = strtol(s, 0, 10);
-	s += 2;
-	if(isdigit(s[0]) && isdigit(s[1]))
-		s += 2;
-	else{
-		if(tm.year <= 68)
-			tm.year += 2000;
-		else
-			tm.year += 1900;
-	}
-	if(tm.mday==0 || tm.mday > mday[isleap(tm.year)][tm.mon]){
-		if(cookiedebug)
-			fprint(2, "invalid day of month (%s)\n", os);
-		return -1;
-	}
-	tm.year -= 1900;
-	if(*s++ != ' '){
-		if(cookiedebug)
-			fprint(2, "bad year separator (%s)\n", os);
-		return -1;
-	}
-
-	if(!isdigit(s[0]) || !isdigit(s[1]) || s[2]!=':'
-	|| !isdigit(s[3]) || !isdigit(s[4]) || s[5]!=':'
-	|| !isdigit(s[6]) || !isdigit(s[7]) || s[8]!=' '){
-		if(cookiedebug)
-			fprint(2, "bad time (%s)\n", os);
-		return -1;
-	}
-
-	tm.hour = atoi(s);
-	tm.min = atoi(s+3);
-	tm.sec = atoi(s+6);
-	if(tm.hour >= 24 || tm.min >= 60 || tm.sec >= 60){
-		if(cookiedebug)
-			fprint(2, "invalid time (%s)\n", os);
-		return -1;
-	}
-	s += 9;
-
-	if(cistrcmp(s, "GMT") != 0){
-		if(cookiedebug)
-			fprint(2, "time zone not GMT (%s)\n", os);
-		return -1;
-	}
-	strcpy(tm.zone, "GMT");
-	tm.yday = 0;
-	return tm2sec(&tm);
-}
-
-/*
- * skip linear whitespace.  we're a bit more lenient than RFC2616 2.2.
- */
-static char*
-skipspace(char *s)
-{
-	while(*s=='\r' || *s=='\n' || *s==' ' || *s=='\t')
-		s++;
-	return s;
-}
-
-/*
- * Try to identify old netscape headers.
- * The old headers:
- *	- didn't allow spaces around the '='
- *	- used an 'Expires' attribute
- *	- had no 'Version' attribute
- *	- had no quotes
- *	- allowed whitespace in values
- *	- apparently separated attr/value pairs with ';' exclusively
- */
-static int
-isnetscape(char *hdr)
-{
-	char *s;
-
-	for(s=hdr; (s=strchr(s, '=')) != nil; s++){
-		if(isspace(s[1]) || (s > hdr && isspace(s[-1])))
-			return 0;
-		if(s[1]=='"')
-			return 0;
-	}
-	if(cistrstr(hdr, "version="))
-		return 0;
-	return 1;
-}
-
-/*
- * Parse HTTP response headers, adding cookies to jar.
- * Overwrites the headers.
- */
-static char* parsecookie(Cookie*, char*, char**, int, char*, char*);
-static int
-parsehttp(Jar *jar, char *hdr, char *dom, char *path)
-{
-	static char setcookie[] = "Set-Cookie:";
-	char *e, *p, *nextp;
-	Cookie c;
-	int isns, n;
-
-	isns = isnetscape(hdr);
-	n = 0;
-	for(p=hdr; p; p=nextp){
-		p = skipspace(p);
-		if(*p == '\0')
-			break;
-		nextp = strchr(p, '\n');
-		if(nextp != nil)
-			*nextp++ = '\0';
-		if(cistrncmp(p, setcookie, strlen(setcookie)) != 0)
-			continue;
-		if(cookiedebug)
-			fprint(2, "%s\n", p);
-		p = skipspace(p+strlen(setcookie));
-		for(; *p; p=skipspace(p)){
-			if((e = parsecookie(&c, p, &p, isns, dom, path)) != nil){
-				if(cookiedebug)
-					fprint(2, "parse cookie: %s\n", e);
-				break;
-			}
-			if((e = isbadcookie(&c, dom, path)) != nil){
-				if(cookiedebug)
-					fprint(2, "reject cookie: %s\n", e);
-				continue;
-			}
-			addcookie(jar, &c);
-			n++;
-		}
-	}
-
-	return n;
-}
-
-static char*
-skipquoted(char *s)
-{
-	/*
-	 * Sec 2.2 of RFC2616 defines a "quoted-string" as:
-	 *
-	 *  quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
-	 *  qdtext         = <any TEXT except <">>
-	 *  quoted-pair    = "\" CHAR
-	 *
-	 * TEXT is any octet except CTLs, but including LWS;
-	 * LWS is [CR LF] 1*(SP | HT);
-	 * CHARs are ASCII octets 0-127;  (NOTE: we reject 0's)
-	 * CTLs are octets 0-31 and 127;
-	 */
-	if(*s != '"')
-		return s;
-
-	for(s++; 32 <= *s && *s < 127 && *s != '"'; s++)
-		if(*s == '\\' && *(s+1) != '\0')
-			s++;
-	return s;
-}
-
-static char*
-skiptoken(char *s)
-{
-	/*
-	 * Sec 2.2 of RFC2616 defines a "token" as
- 	 *  1*<any CHAR except CTLs or separators>;
-	 * CHARs are ASCII octets 0-127;
-	 * CTLs are octets 0-31 and 127;
-	 * separators are "()<>@,;:\/[]?={}", double-quote, SP (32), and HT (9)
-	 */
-	while(32 <= *s && *s < 127 && strchr("()<>@,;:[]?={}\" \t\\", *s)==nil)
-		s++;
-
-	return s;
-}
-
-static char*
-skipvalue(char *s, int isns)
-{
-	char *t;
-
-	/*
-	 * An RFC2109 value is an HTTP token or an HTTP quoted string.
-	 * Netscape servers ignore the spec and rely on semicolons, apparently.
-	 */
-	if(isns){
-		if((t = strchr(s, ';')) == nil)
-			t = s+strlen(s);
-		return t;
-	}
-	if(*s == '"')
-		return skipquoted(s);
-	return skiptoken(s);
-}
-
-/*
- * RMID=80b186bb64c03c65fab767f8; expires=Monday, 10-Feb-2003 04:44:39 GMT; 
- *	path=/; domain=.nytimes.com
- */
-static char*
-parsecookie(Cookie *c, char *p, char **e, int isns, char *dom, char *path)
-{
-	int i, done;
-	char *t, *u, *attr, *val;
-
-	memset(c, 0, sizeof *c);
-	c->expire = ~0;
-
-	/* NAME=VALUE */
-	t = skiptoken(p);
-	c->name = p;
-	p = skipspace(t);
-	if(*p != '='){
-	Badname:
-		return "malformed cookie: no NAME=VALUE";
-	}
-	*t = '\0';
-	p = skipspace(p+1);
-	t = skipvalue(p, isns);
-	if(*t)
-		*t++ = '\0';
-	c->value = p;
-	p = skipspace(t);
-	if(c->name[0]=='\0' || c->value[0]=='\0')
-		goto Badname;
-
-	done = 0;
-	for(; *p && !done; p=skipspace(p)){
-		attr = p;
-		t = skiptoken(p);
-		u = skipspace(t);
-		switch(*u){
-		case '\0':
-			*t = '\0';
-			val = p = u;
-			break;
-		case ';':
-			*t = '\0';
-			val = "";
-			p = u+1;
-			break;
-		case '=':
-			*t = '\0';
-			val = skipspace(u+1);
-			p = skipvalue(val, isns);
-			if(*p==',')
-				done = 1;
-			if(*p)
-				*p++ = '\0';
-			break;
-		case ',':
-			if(!isns){
-				val = "";
-				p = u;
-				*p++ = '\0';
-				done = 1;
-				break;
-			}
-		default:
-			if(cookiedebug)
-				fprint(2, "syntax: %s\n", p);
-			return "syntax error";
-		}
-		for(i=0; i<nelem(stab); i++)
-			if(stab[i].ishttp && cistrcmp(stab[i].s, attr)==0)
-				*(char**)((uintptr)c+stab[i].offset) = val;
-		if(cistrcmp(attr, "expires") == 0){
-			if(!isns)
-				return "non-netscape cookie has Expires tag";
-			if(!val[0])
-				return "bad expires tag";
-			c->expire = strtotime(val);
-			if(c->expire == ~0)
-				return "cannot parse netscape expires tag";
-		}
-		if(cistrcmp(attr, "max-age") == 0)
-			c->expire = time(0)+atoi(val);
-		if(cistrcmp(attr, "secure") == 0)
-			c->secure = 1;
-	}
-	*e = p;
-
-	if(c->dom)
-		c->explicitdom = 1;
-	else
-		c->dom = dom;
-	if(c->path)
-		c->explicitpath = 1;
-	else
-		c->path = path;
-	c->netscapestyle = isns;
-
-	return nil;
-}
-
-Jar *jar;
-
-typedef struct Aux Aux;
-struct Aux
-{
-	char *dom;
-	char *path;
-	char *inhttp;
-	char *outhttp;
-	char *ctext;
-	int rdoff;
-};
-enum
-{
-	MaxCtext = 16*1024*1024,
-};
-
-void
-cookieopen(Req *r)
-{
-	char *s, *es;
-	int i, sz;
-	Aux *a;
-
-	syncjar(jar);
-	a = emalloc(sizeof(Aux));
-	r->fid->aux = a;
-	if(r->ifcall.mode&OTRUNC){
-		a->ctext = emalloc(1);
-		a->ctext[0] = '\0';
-	}else{
-		sz = 256*jar->nc+1024;	/* BUG should do better */
-		a->ctext = emalloc(sz);
-		a->ctext[0] = '\0';
-		s = a->ctext;
-		es = s+sz;
-		for(i=0; i<jar->nc; i++)
-			s = seprint(s, es, "%K\n", &jar->c[i]);
-	}
-	respond(r, nil);
-}
-
-void
-cookieread(Req *r)
-{
-	Aux *a;
-
-	a = r->fid->aux;
-	readstr(r, a->ctext);
-	respond(r, nil);
-}
-
-void
-cookiewrite(Req *r)
-{
-	Aux *a;
-	int sz;
-
-	a = r->fid->aux;
-	sz = r->ifcall.count+r->ifcall.offset;
-	if(sz > strlen(a->ctext)){
-		if(sz >= MaxCtext){
-			respond(r, "cookie file too large");
-			return;
-		}
-		a->ctext = erealloc9p(a->ctext, sz+1);
-		a->ctext[sz] = '\0';
-	}
-	memmove(a->ctext+r->ifcall.offset, r->ifcall.data, r->ifcall.count);
-	r->ofcall.count = r->ifcall.count;
-	respond(r, nil);
-}
-
-void
-cookieclunk(Fid *fid)
-{
-	char *p, *nextp;
-	Aux *a;
-	int i;
-
-	a = fid->aux;
-	if(a == nil)
-		return;
-	for(i=0; i<jar->nc; i++)
-		jar->c[i].mark = 1;
-	for(p=a->ctext; *p; p=nextp){
-		if((nextp = strchr(p, '\n')) != nil)
-			*nextp++ = '\0';
-		else
-			nextp = "";
-		addtojar(jar, p, 0);
-	}
-	for(i=0; i<jar->nc; i++)
-		if(jar->c[i].mark)
-			delcookie(jar, &jar->c[i]);
-	syncjar(jar);
-	free(a->dom);
-	free(a->path);
-	free(a->inhttp);
-	free(a->outhttp);
-	free(a->ctext);
-	free(a);
-}
-
-void
-closecookies(void)
-{
-	closejar(jar);
-}
-
-void
-initcookies(char *file)
-{
-	char *home;
-
-	fmtinstall('J', jarfmt);
-	fmtinstall('K', cookiefmt);
-
-	if(file == nil){
-		home = getenv("home");
-		if(home == nil)
-			sysfatal("no cookie file specified and no $home");
-		file = emalloc(strlen(home)+30);
-		strcpy(file, home);
-		strcat(file, "/lib/webcookies");
-		free(home);
-	}
-	jar = readjar(file);
-	if(jar == nil)
-		sysfatal("readjar: %r");
-}
-
-void
-httpsetcookie(char *hdr, char *dom, char *path)
-{
-	char *t;
-
-	path = estrdup(path && path[0] ? path : "/");
-	if((t = strchr(path, '?')) != 0)
-		*t = '\0';
-	t = strrchr(path, '/');
-	if(t && t != path)
-		*t = '\0';
-	parsehttp(jar, hdr, dom, path);
-	syncjar(jar);
-	free(path);
-}
-
-char*
-httpcookies(char *dom, char *path, int issecure)
-{
-	char *s;
-	Jar *j;
-
-	syncjar(jar);
-	j = cookiesearch(jar, dom, path, issecure);
-	s = smprint("%J", j);
-	closejar(j);
-	return s;
-}
--- a/sys/src/cmd/webfs/dat.h
+++ b/sys/src/cmd/webfs/dat.h
@@ -1,104 +1,67 @@
-typedef struct Client Client;
-typedef struct Ctl Ctl;
-typedef struct Ibuf Ibuf;
 typedef struct Url Url;
+typedef struct Buq Buq;
+typedef struct Buf Buf;
+typedef struct Key Key;
 
-/* simple buffered i/o for network connections; shared by http, ftp */
-struct Ibuf
-{
-	int fd;
-	Ioproc *io;
-	char buf[4096];
-	char *rp, *wp;
-};
+typedef struct {
+	char	*s1;
+	char	*s2;
+} Str2;
 
-struct Ctl
-{
-	int	acceptcookies;
-	int	sendcookies;
-	int	redirectlimit;
-	char	*useragent;
-};
+/* 9p */
+typedef struct Req Req;
 
-struct Client
+struct Url
 {
-	Url	*url;
-	Url	*baseurl;
-
-	Ctl ctl;
-	Channel *creq;	/* chan(Req*) */
-	int num;
-	int plumbed;
-	char *contenttype;
-	char *postbody;
-	char *redirect;
-	char *authenticate;
-	char *ext;
-	int npostbody;
-	int havepostbody;
-	int iobusy;
-	int bodyopened;
-	Ioproc *io;
-	int ref;
-	void *aux;
+	char	*scheme;
+	char	*user;
+	char	*pass;
+	char	*host;
+	char	*port;
+	char	*path;
+	char	*query;
+	char	*fragment;
 };
 
-/*
- * If ischeme is USunknown, then the given URL is a relative
- * URL which references the "current document" in the context of the base.
- * If this is the case, only the "fragment" and "url" members will have
- * meaning, and the given URL structure may not be used as a base URL itself.
- */
-enum
+struct Buf
 {
-	USunknown,
-	UShttp,
-	UShttps,
-	USftp,
-	USfile,
-	UScurrent,
+	Buf	*next;
+	uchar	*rp;
+	uchar	*ep;
+	Req	*wreq;
+	uchar	end[];
 };
 
-struct Url
+struct Key
 {
-	int		ischeme;
-	char*	url;
-	char*	scheme;
-	int		(*open)(Client*, Url*);
-	int		(*read)(Client*, Req*);
-	void		(*close)(Client*);
-	char*	schemedata;
-	char*	authority;
-	char*	user;
-	char*	passwd;
-	char*	host;
-	char*	port;
-	char*	path;
-	char*	query;
-	char*	fragment;
-	union {
-		struct {
-			char *page_spec;
-		} http;
-		struct {
-			char *path_spec;
-			char *type;
-		} ftp;
-	};
+	Key	*next;
+	char	*val;
+	char	key[];
 };
 
-enum
+struct Buq
 {
-	STACK = 32*1024,  /* was 16*1024; there are big arrays on the stack */
-};
+	Ref;
+	QLock;
 
-extern	Client**	client;
-extern	int		cookiedebug;
-extern	Srv		fs;
-extern	int		fsdebug;
-extern	Ctl		globalctl;
-extern	int		nclient;
-extern	int		urldebug;
-extern	int		httpdebug;
-extern	char*	status[];
+	Url	*url;
+	Key	*hdr;
+	char	*error;
 
+	int	closed;
+	int	limit;
+	int	size;
+
+	/* write buffers */
+	Buf	*bh;
+	Buf	**bt;
+
+	/* read requests */
+	Req	*rh;
+	Req	**rt;
+
+	Rendez	rz;
+};
+
+int	debug;
+Url	*proxy;
--- a/sys/src/cmd/webfs/fns.h
+++ b/sys/src/cmd/webfs/fns.h
@@ -1,62 +1,35 @@
-/* buf.c */
-void		initibuf(Ibuf*, Ioproc*, int);
-int		readibuf(Ibuf*, char*, int);
-void		unreadline(Ibuf*, char*);
-int		readline(Ibuf*, char*, int);
+/* sub */
+void*	emalloc(int n);
+char*	estrdup(char *s);
 
-/* client.c */
-int		newclient(int);
-void		closeclient(Client*);
-void		clonectl(Ctl*);
-int		ctlwrite(Req*, Ctl*, char*, char*);
-int		clientctlwrite(Req*, Client*, char*, char*);
-int		globalctlwrite(Req*, char*, char*);
-void		ctlread(Req*, Client*);
-void		globalctlread(Req*);
-void		plumburl(char*, char*);
+Key*	addkey(Key *h, char *key, char *val);
+Key*	delkey(Key *h, char *key);
+char*	lookkey(Key *k, char *key);
+Key*	parsehdr(char *s);
+char*	unquote(char *s, char **ps);
 
-/* cookies.c */
-void		cookieread(Req*);
-void		cookiewrite(Req*);
-void		cookieopen(Req*);
-void		cookieclunk(Fid*);
-void		initcookies(char*);
-void		closecookies(void);
-void		httpsetcookie(char*, char*, char*);
-char*	httpcookies(char*, char*, int);
+/* url */
+#pragma	varargck type "U" Url*
+#pragma varargck type "E" Str2
 
-/* fs.c */
-void		initfs(void);
+int	Efmt(Fmt*);
+int	Ufmt(Fmt*);
+char*	Upath(Url *);
+Url*	url(char *s, Url *b);
+Url*	saneurl(Url *u);
+int	matchurl(Url *u, Url *s);
+void	freeurl(Url *u);
 
-/* http.c */
-int		httpopen(Client*, Url*);
-int		httpread(Client*, Req*);
-void		httpclose(Client*);
+/* buq */
+int	buread(Buq *q, void *v, int l);
+int	buwrite(Buq *q, void *v, int l);
+void	buclose(Buq *q, char *error);
+Buq*	bualloc(int limit);
+void	bufree(Buq *q);
 
-/* io.c */
-int		iotlsdial(Ioproc*, char*, char*, char*, int*, int);
-int		ioprint(Ioproc*, int, char*, ...);
-#pragma varargck argpos ioprint 3
+void	bureq(Buq *q, Req *r);
+void	buflushreq(Buq *q, Req *r);
 
-/* plumb.c */
-void	plumbinit(void);
-void	plumbstart(void);
-void	replumb(Client*);
-
-/* url.c */
-Url*		parseurl(char*, Url*);
-void		freeurl(Url*);
-void		rewriteurl(Url*);
-int		seturlquery(Url*, char*);
-Url*		copyurl(Url*);
-char*	escapeurl(char*, char *);
-char*	unescapeurl(char*, char *);
-void		initurl(void);
-
-/* util.c */
-char*	estrdup(char*);
-char*	estrmanydup(char*, ...);
-char*	estredup(char*, char*);
-void*	emalloc(uint);
-void*	erealloc(void*, uint);
-char*	strlower(char*);
+/* http */
+void flushauth(Url *u, char *t);
+void http(char *m, Url *u, Key *shdr, Buq *qbody, Buq *qpost);
--- a/sys/src/cmd/webfs/fs.c
+++ b/sys/src/cmd/webfs/fs.c
@@ -1,617 +1,728 @@
-/*
- * Web file system.  Conventionally mounted at /mnt/web
- *
- *	ctl				send control messages (might go away)
- *	cookies			list of cookies, editable
- *	clone			open and read to obtain new connection
- *	n				connection directory
- *		ctl				control messages (like get url)
- *		body				retrieved data
- *		content-type		mime content-type of body
- *		postbody			data to be posted
- *		parsed			parsed version of url
- * 			url				entire url
- *			scheme			http, ftp, etc.
- *			host				hostname
- *			path				path on host
- *			query			query after path
- *			fragment			#foo anchor reference
- *			user				user name (ftp)
- *			password			password (ftp)
- *			ftptype			transfer mode (ftp)
- */
-
 #include <u.h>
 #include <libc.h>
-#include <bio.h>
-#include <ip.h>
-#include <plumb.h>
-#include <thread.h>
+#include <ctype.h>
 #include <fcall.h>
+#include <thread.h>
 #include <9p.h>
+
 #include "dat.h"
 #include "fns.h"
 
-int fsdebug;
+typedef struct Webfid Webfid;
+typedef struct Client Client;
 
-enum
+struct Client
 {
-	Qroot,
-	Qrootctl,
-	Qclone,
-	Qcookies,
-	Qclient,
-	Qctl,
-	Qbody,
-	Qbodyext,
-	Qcontenttype,
-	Qpostbody,
-	Qparsed,
-	Qurl,
-	Qscheme,
-	Qschemedata,
-	Quser,
-	Qpasswd,
-	Qhost,
-	Qport,
-	Qpath,
-	Qquery,
-	Qfragment,
-	Qftptype,
-	Qend,
-};
+	Ref;
 
-#define PATH(type, n)	((type)|((n)<<8))
-#define TYPE(path)		((int)(path) & 0xFF)
-#define NUM(path)		((uint)(path)>>8)
+	char	request[16];
+	Url	*baseurl;
+	Url	*url;
+	Key	*hdr;
 
-Channel *creq;
-Channel *creqwait;
-Channel *cclunk;
-Channel *cclunkwait;
+	int	obody;	/* body opend */
+	int	cbody;	/* body closed */
+	Buq	*qbody;
+};
 
-typedef struct Tab Tab;
-struct Tab
+struct Webfid
 {
-	char *name;
-	ulong mode;
-	int offset;
+	int	level;
+
+	Client	*client;
+	Key	*key;	/* copy for Qheader */
+	Buq	*buq;	/* reference for Qbody, Qpost */
 };
 
-Tab tab[] =
-{
-	"/",			DMDIR|0555,		0,
-	"ctl",			0666,			0,
-	"clone",		0666,			0,
-	"cookies",		0666,			0,
-	"XXX",		DMDIR|0555,		0,
-	"ctl",			0666,			0,
-	"body",		0444,			0,
-	"XXX",		0444,			0,
-	"contenttype",	0444,			0,
-	"postbody",	0666,			0,
-	"parsed",		DMDIR|0555,		0,
-	"url",			0444,			offsetof(Url, url),
-	"scheme",		0444,			offsetof(Url, scheme),
-	"schemedata",	0444,			offsetof(Url, schemedata),
-	"user",		0444,			offsetof(Url, user),
-	"passwd",		0444,			offsetof(Url, passwd),
-	"host",		0444,			offsetof(Url, host),
-	"port",		0444,			offsetof(Url, port),
-	"path",		0444,			offsetof(Url, path),
-	"query",		0444,			offsetof(Url, query),
-	"fragment",	0444,			offsetof(Url, fragment),
-	"ftptype",		0444,			offsetof(Url, ftp.type),
+enum {
+	Qroot,
+		Qrctl,
+		Qclone,
+		Qclient,
+			Qctl,
+			Qbody,
+			Qpost,
+			Qparsed,
+				Qurl,
+				Qurlschm,
+				Qurluser,
+				Qurlpass,
+				Qurlhost,
+				Qurlport,
+				Qurlpath,
+				Qurlqwry,
+				Qurlfrag,
+			Qheader,
 };
 
-ulong time0;
+static char *nametab[] = {
+	"/",
+		"ctl",
+		"clone",
+		nil,
+			"ctl",
+			"body",
+			"postbody",
+			"parsed",
+				"url",
+				"scheme",
+				"user",
+				"passwd",
+				"host",
+				"port",
+				"path",
+				"query",
+				"fragment",
+			nil,
+};
 
-static void
-fillstat(Dir *d, uvlong path, ulong length, char *ext)
+static long time0;
+static char *user;
+static Client client[64];
+static int nclient;
+
+#define	CLIENTID(c)	(((Client*)(c)) - client)
+
+Client*
+newclient(void)
 {
-	Tab *t;
-	int type;
-	char buf[32];
+	Client *cl;
+	int i;
 
-	memset(d, 0, sizeof(*d));
-	d->uid = estrdup("web");
-	d->gid = estrdup("web");
-	d->qid.path = path;
-	d->atime = d->mtime = time0;
-	d->length = length;
-	type = TYPE(path);
-	t = &tab[type];
-	if(type == Qbodyext) {
-		snprint(buf, sizeof buf, "body.%s", ext == nil ? "xxx" : ext);
-		d->name = estrdup(buf);
+	for(i = 0; i < nclient; i++)
+		if(client[i].ref == 0)
+			break;
+	if(i >= nelem(client))
+		return nil;
+	if(i == nclient)
+		nclient++;
+	cl = &client[i];
+	incref(cl);
+
+	cl->request[0] = 0;
+	cl->baseurl = nil;
+	cl->url = nil;
+	cl->hdr = nil;
+	cl->qbody = nil;
+	
+	return cl;
+}
+
+void
+freeclient(Client *cl)
+{
+	Key *k;
+
+	if(cl == nil || decref(cl))
+		return;
+
+	buclose(cl->qbody, 0);
+	bufree(cl->qbody);
+
+	while(k = cl->hdr){
+		cl->hdr = k->next;
+		free(k);
 	}
-	else if(t->name)
-		d->name = estrdup(t->name);
-	else{	/* client directory */
-		snprint(buf, sizeof buf, "%ud", NUM(path));
-		d->name = estrdup(buf);
-	}
-	d->qid.type = t->mode>>24;
-	d->mode = t->mode;
+
+	freeurl(cl->url);
+	freeurl(cl->baseurl);
+
+	memset(cl, 0, sizeof(*cl));
 }
 
-static void
-fsstat(Req *r)
+static Url*
+clienturl(Client *cl)
 {
-	fillstat(&r->d, r->fid->qid.path, 0, nil);
-	respond(r, nil);
+	static Url nullurl;
+
+	if(cl->qbody && cl->qbody->url)
+		return cl->qbody->url;
+	if(cl->url)
+		return cl->url;
+	return &nullurl;
 }
 
-static int
-rootgen(int i, Dir *d, void*)
+static void*
+wfaux(Webfid *f)
 {
-	char buf[32];
+	if(f->level < Qclient)
+		return nil;
+	else if(f->level < Qurl)
+		return f->client;
+	else if(f->level < Qheader)
+		return clienturl(f->client);
+	else
+		return f->key;
+}
 
-	i += Qroot+1;
-	if(i < Qclient){
-		fillstat(d, i, 0, nil);
-		return 0;
+static void
+fsmkqid(Qid *q, int level, void *aux)
+{
+	q->type = 0;
+	q->vers = 0;
+	switch(level){
+	case Qroot:
+	case Qparsed:
+	case Qclient:
+		q->type = QTDIR;
+	default:
+		q->path = (level<<24) | (((ulong)aux ^ time0) & 0x00ffffff);
 	}
-	i -= Qclient;
-	if(i < nclient){
-		fillstat(d, PATH(Qclient, i), 0, nil);
-		snprint(buf, sizeof buf, "%d", i);
-		free(d->name);
-		d->name = estrdup(buf);
-		return 0;
-	}
-	return -1;
 }
 
-static int
-clientgen(int i, Dir *d, void *aux)
+static char*
+fshdrname(char *s)
 {
-	Client *c;
+	char *k, *w;
 
-	c = aux;
-	i += Qclient+1;
-	if(i <= Qparsed){
-		fillstat(d, PATH(i, c->num), 0, c->ext);
-		return 0;
-	}
-	return -1;
+	for(k=w=s; *k; k++)
+		if(isalnum(*k))
+			*w++ = tolower(*k);
+	*w = 0;
+	return s;
 }
 
 static int
-parsedgen(int i, Dir *d, void *aux)
+urlstr(char *buf, int nbuf, Url *u, int level)
 {
-	Client *c;
+	char *s;
 
-	c = aux;
-	i += Qparsed+1;
-	if(i < Qend){
-		fillstat(d, PATH(i, c->num), 0, nil);
+	if(level == Qurl)
+		return snprint(buf, nbuf, "%U", u);
+	if(level == Qurlpath)
+		return snprint(buf, nbuf, "%s", Upath(u));
+	if((s = (&u->scheme)[level - Qurlschm]) == nil){
+		buf[0] = 0;
 		return 0;
 	}
-	return -1;
+	return snprint(buf, nbuf, "%s", s);
 }
 
+
 static void
-fsread(Req *r)
+fsmkdir(Dir *d, int level, void *aux)
 {
-	char *s;
-	char e[ERRMAX];
-	Client *c;
-	ulong path;
+	char buf[1024];
 
-	path = r->fid->qid.path;
-	switch(TYPE(path)){
-	default:
-		snprint(e, sizeof e, "bug in webfs path=%lux\n", path);
-		respond(r, e);
+	memset(d, 0, sizeof(*d));
+	fsmkqid(&d->qid, level, aux);
+	d->mode = 0444;
+	d->atime = d->mtime = time0;
+	d->uid = estrdup(user);
+	d->gid = estrdup(user);
+	d->muid = estrdup(user);
+	if(d->qid.type == QTDIR)
+		d->mode |= DMDIR | 0111;
+	switch(level){
+	case Qheader:
+		d->name = fshdrname(estrdup(((Key*)aux)->key));
+		d->length = strlen(((Key*)aux)->val);
 		break;
-
-	case Qroot:
-		dirread9p(r, rootgen, nil);
-		respond(r, nil);
-		break;
-
-	case Qrootctl:
-		globalctlread(r);
-		break;
-
-	case Qcookies:
-		cookieread(r);
-		break;
-
 	case Qclient:
-		dirread9p(r, clientgen, client[NUM(path)]);
-		respond(r, nil);
+		snprint(buf, sizeof(buf), "%ld", CLIENTID(aux));
+		d->name = estrdup(buf);
 		break;
-
 	case Qctl:
-		ctlread(r, client[NUM(path)]);
-		break;
-
-	case Qcontenttype:
-		c = client[NUM(path)];
-		if(c->contenttype == nil)
-			r->ofcall.count = 0;
-		else
-			readstr(r, c->contenttype);
-		respond(r, nil);
-		break;
-
-	case Qpostbody:
-		c = client[NUM(path)];
-		readbuf(r, c->postbody, c->npostbody);
-		respond(r, nil);
-		break;
-		
-	case Qbody:
-	case Qbodyext:
-		c = client[NUM(path)];
-		if(c->iobusy){
-			respond(r, "already have i/o pending");
-			break;
+	case Qrctl:
+	case Qclone:
+		d->mode = 0666;
+		if(0){
+	case Qpost:
+		d->mode = 0222;
 		}
-		c->iobusy = 1;
-		sendp(c->creq, r);
-		break;
+	default:
+		d->name = estrdup(nametab[level]);
+		if(level >= Qurl && level <= Qurlfrag)
+			d->length = urlstr(buf, sizeof(buf), (Url*)aux, level);
+	}
+}
 
-	case Qparsed:
-		dirread9p(r, parsedgen, client[NUM(path)]);
-		respond(r, nil);
-		break;
+static void
+fsattach(Req *r)
+{
+	Webfid *f;
 
-	case Qurl:
-	case Qscheme:
-	case Qschemedata:
-	case Quser:
-	case Qpasswd:
-	case Qhost:
-	case Qport:
-	case Qpath:
-	case Qquery:
-	case Qfragment:
-	case Qftptype:
-		c = client[NUM(path)];
-		r->ofcall.count = 0;
-		if(c->url != nil
-		&& (s = *(char**)((uintptr)c->url+tab[TYPE(path)].offset)) != nil)
-			readstr(r, s);
-		respond(r, nil);
-		break;
+	if(r->ifcall.aname && r->ifcall.aname[0]){
+		respond(r, "invalid attach specifier");
+		return;
 	}
+	f = emalloc(sizeof(*f));
+	f->level = Qroot;
+	fsmkqid(&r->fid->qid, f->level, wfaux(f));
+	r->ofcall.qid = r->fid->qid;
+	r->fid->aux = f;
+	respond(r, nil);
 }
 
 static void
-fswrite(Req *r)
+fsstat(Req *r)
 {
-	int m;
-	ulong path;
-	char e[ERRMAX], *buf, *cmd, *arg;
-	Client *c;
+	Webfid *f;
 
-	path = r->fid->qid.path;
-	switch(TYPE(path)){
-	default:
-		snprint(e, sizeof e, "bug in webfs path=%lux\n", path);
-		respond(r, e);
-		break;
+	f = r->fid->aux;
+	fsmkdir(&r->d, f->level, wfaux(f));
+	respond(r, nil);
+}
 
-	case Qcookies:
-		cookiewrite(r);
-		break;
+static char*
+fswalk1(Fid *fid, char *name, Qid *qid)
+{
+	Webfid *f;
+	int i, j;
 
-	case Qrootctl:
-	case Qctl:
-		if(r->ifcall.count >= 1024){
-			respond(r, "ctl message too long");
-			return;
-		}
-		buf = estredup(r->ifcall.data, (char*)r->ifcall.data+r->ifcall.count);
-		cmd = buf;
-		arg = strpbrk(cmd, "\t ");
-		if(arg){
-			*arg++ = '\0';
-			arg += strspn(arg, "\t ");
-		}else
-			arg = "";
-		r->ofcall.count = r->ifcall.count;
-		if(TYPE(path)==Qrootctl){
-			if(!ctlwrite(r, &globalctl, cmd, arg)
-			&& !globalctlwrite(r, cmd, arg))
-				respond(r, "unknown control command");
-		}else{
-			c = client[NUM(path)];
-			if(!ctlwrite(r, &c->ctl, cmd, arg)
-			&& !clientctlwrite(r, c, cmd, arg))
-				respond(r, "unknown control command");
-		}
-		free(buf);
-		break;
+	if(!(fid->qid.type&QTDIR))
+		return "walk in non-directory";
 
-	case Qpostbody:
-		c = client[NUM(path)];
-		if(c->bodyopened){
-			respond(r, "cannot write postbody after opening body");
+	f = fid->aux;
+	if(strcmp(name, "..") == 0){
+		switch(f->level){
+		case Qroot:
 			break;
-		}
-		if(r->ifcall.offset >= 128*1024*1024){	/* >128MB is probably a mistake */
-			respond(r, "offset too large");
+		case Qclient:
+			freeclient(f->client);
+			f->client = nil;
 			break;
+		default:
+			if(f->level > Qparsed)
+				f->level = Qparsed;
+			else
+				f->level = Qclient;
 		}
-		m = r->ifcall.offset + r->ifcall.count;
-		if(c->npostbody < m){
-			c->postbody = erealloc(c->postbody, m);
-			memset(c->postbody+c->npostbody, 0, m-c->npostbody);
-			c->npostbody = m;
+	} else {
+		for(i=f->level+1; i < nelem(nametab); i++){
+			if(nametab[i]){
+				if(strcmp(name, nametab[i]) == 0)
+					break;
+				if(i == Qbody && strncmp(name, "body.", 5) == 0)
+					break;
+			}
+			if(i == Qclient){
+				j = atoi(name);
+				if(j >= 0 && j < nclient){
+					f->client = &client[j];
+					incref(f->client);
+					break;
+				}
+			}
+			if(i == Qheader && f->client && f->client->qbody){
+				char buf[128];
+				Key *k;
+
+				for(k = f->client->qbody->hdr; k; k = k->next){
+					strncpy(buf, k->key, sizeof(buf));
+					if(!strcmp(name, fshdrname(buf)))
+						break;
+				}
+				if(k != nil){
+					/* need to copy as key is owned by qbody wich might go away */
+					f->key = addkey(0, k->key, k->val);
+					break;
+				}
+			}
 		}
-		memmove(c->postbody+r->ifcall.offset, r->ifcall.data, r->ifcall.count);
-		r->ofcall.count = r->ifcall.count;
-		respond(r, nil);
-		break;
+		if(i >= nelem(nametab))
+			return "directory entry not found";
+		f->level = i;
 	}
+	fsmkqid(qid, f->level, wfaux(f));
+	fid->qid = *qid;
+	return nil;
 }
 
+static char*
+fsclone(Fid *oldfid, Fid *newfid)
+{
+	Webfid *f, *o;
+
+	o = oldfid->aux;
+	if(o == nil || o->key || o->buq)
+		return "bad fid";
+	f = emalloc(sizeof(*f));
+	memmove(f, o, sizeof(*f));
+	if(f->client)
+		incref(f->client);
+	newfid->aux = f;
+	return nil;
+}
+
 static void
 fsopen(Req *r)
 {
-	static int need[4] = { 4, 2, 6, 1 };
-	ulong path;
-	int n;
-	Client *c;
-	Tab *t;
+	Webfid *f;
+	Client *cl;
 
-	/*
-	 * lib9p already handles the blatantly obvious.
-	 * we just have to enforce the permissions we have set.
-	 */
-	path = r->fid->qid.path;
-	t = &tab[TYPE(path)];
-	n = need[r->ifcall.mode&3];
-	if((n&t->mode) != n){
-		respond(r, "permission denied");
+	f = r->fid->aux;
+	cl = f->client;
+	switch(f->level){
+	case Qclone:
+		if((cl = newclient()) == nil){
+			respond(r, "no more clients");
+			return;
+		}
+		f->level = Qctl;
+		f->client = cl;
+		fsmkqid(&r->fid->qid, f->level, wfaux(f));
+		r->ofcall.qid = r->fid->qid;
+		break;
+	case Qpost:
+		if(cl->qbody && !cl->cbody){
+		Inuse:
+			respond(r, "client in use");
+			return;
+		}
+	case Qbody:
+		if(cl->obody)
+			goto Inuse;
+		if(cl->cbody){
+			bufree(cl->qbody);
+			cl->qbody = nil;
+			cl->cbody = 0;
+		}
+		if(cl->qbody == nil){
+			char *m;
+
+			if(cl->url == nil){
+				respond(r, "no url set");
+				return;
+			}
+			cl->qbody = bualloc(16*1024);
+			if(f->level != Qbody){
+				f->buq = bualloc(64*1024);
+				if(!lookkey(cl->hdr, "Content-Type"))
+					cl->hdr = addkey(cl->hdr, "Content-Type", 
+						"application/x-www-form-urlencoded");
+				m = "POST";
+			} else
+				m = "GET";
+			if(cl->request[0])
+				m = cl->request;
+			if(!lookkey(cl->hdr, "Connection"))
+				cl->hdr = addkey(cl->hdr, "Connection", "keep-alive");
+			if(!lookkey(cl->hdr, "User-Agent"))
+				cl->hdr = addkey(cl->hdr, "User-Agent", "webfs/0.1 (Plan 9 Front)");
+			http(m, cl->url, cl->hdr, cl->qbody, f->buq);
+			cl->request[0] = 0;
+			cl->url = nil;
+			cl->hdr = nil;
+		}
+		if(f->buq)
+			break;
+		cl->obody = 1;
+		incref(cl->qbody);
+		bureq(f->buq = cl->qbody, r);
 		return;
 	}
+	respond(r, nil);
+}
 
-	switch(TYPE(path)){
-	case Qcookies:
-		cookieopen(r);
-		break;
+static int
+rootgen(int i, Dir *d, void *)
+{
+	i += Qroot+1;
+	if(i < Qclient){
+		fsmkdir(d, i, 0);
+		return 0;
+	}
+	i -= Qclient;
+	if(i < nclient){
+		fsmkdir(d, Qclient, &client[i]);
+		return 0;
+	}
+	return -1;
+}
 
-	case Qpostbody:
-		c = client[NUM(path)];
-		c->havepostbody++;
-		c->ref++;
-		respond(r, nil);
-		break;
+static int
+clientgen(int i, Dir *d, void *aux)
+{
+	i += Qclient+1;
+	if(i > Qparsed){
+		Client *cl = aux;
+		Key *k;
 
-	case Qbody:
-	case Qbodyext:
-		c = client[NUM(path)];
-		if(c->url == nil){
-			respond(r, "url is not yet set");
-			break;
-		}
-		c->bodyopened = 1;
-		c->ref++;
-		sendp(c->creq, r);
-		break;
-
-	case Qclone:
-		n = newclient(0);
-		path = PATH(Qctl, n);
-		r->fid->qid.path = path;
-		r->ofcall.qid.path = path;
-		if(fsdebug)
-			fprint(2, "open clone => path=%lux\n", path);
-		t = &tab[Qctl];
-		/* fall through */
-	default:
-		if(t-tab >= Qclient)
-			client[NUM(path)]->ref++;
-		respond(r, nil);
-		break;
+		i -= Qparsed+1;
+		if(cl == nil || cl->qbody == nil)
+			return -1;
+		for(k = cl->qbody->hdr; i > 0 && k; i--, k = k->next)
+			;
+		if(k == nil || i > 0)
+			return -1;
+		i = Qheader;
+		aux = k;
 	}
+	fsmkdir(d, i, aux);
+	return 0;
 }
 
-static void
-fsdestroyfid(Fid *fid)
+static int
+parsedgen(int i, Dir *d, void *aux)
 {
-	sendp(cclunk, fid);
-	recvp(cclunkwait);
+	i += Qparsed+1;
+	if(i > Qurlfrag)
+		return -1;
+	fsmkdir(d, i, aux);
+	return 0;
 }
 
 static void
-fsattach(Req *r)
+fsread(Req *r)
 {
-	if(r->ifcall.aname && r->ifcall.aname[0]){
-		respond(r, "invalid attach specifier");
+	char buf[1024];
+	Webfid *f;
+
+	f = r->fid->aux;
+	switch(f->level){
+	case Qroot:
+		dirread9p(r, rootgen, nil);
+		respond(r, nil);
 		return;
+	case Qclient:
+		dirread9p(r, clientgen, f->client);
+		respond(r, nil);
+		return;
+	case Qparsed:
+		dirread9p(r, parsedgen, clienturl(f->client));
+		respond(r, nil);
+		return;
+	case Qrctl:
+		buf[0] = 0;
+	String:
+		readstr(r, buf);
+		respond(r, nil);
+		return;
+	case Qctl:
+		snprint(buf, sizeof(buf), "%ld\n", CLIENTID(f->client));
+		goto String;
+	case Qheader:
+		snprint(buf, sizeof(buf), "%s", f->key->val);
+		goto String;
+	case Qurl:
+	case Qurlschm:
+	case Qurluser:
+	case Qurlpass:
+	case Qurlhost:
+	case Qurlport:
+	case Qurlpath:
+	case Qurlqwry:
+	case Qurlfrag:
+		urlstr(buf, sizeof(buf), clienturl(f->client), f->level);
+		goto String;
+	case Qbody:
+		bureq(f->buq, r);
+		return;
 	}
-	r->fid->qid.path = PATH(Qroot, 0);
-	r->fid->qid.type = QTDIR;
-	r->fid->qid.vers = 0;
-	r->ofcall.qid = r->fid->qid;
-	respond(r, nil);
+	respond(r, "not implemented");
 }
 
 static char*
-fswalk1(Fid *fid, char *name, Qid *qid)
+rootctl(char *ctl, char *arg)
 {
-	int i, n;
-	ulong path;
-	char buf[32], *ext;
+	Url *u;
 
-	path = fid->qid.path;
-	if(!(fid->qid.type&QTDIR))
-		return "walk in non-directory";
+	if(debug)
+		fprint(2, "rootctl: %q %q\n", ctl, arg);
 
-	if(strcmp(name, "..") == 0){
-		switch(TYPE(path)){
-		case Qparsed:
-			qid->path = PATH(Qclient, NUM(path));
-			qid->type = tab[Qclient].mode>>24;
-			return nil;
-		case Qclient:
-		case Qroot:
-			qid->path = PATH(Qroot, 0);
-			qid->type = tab[Qroot].mode>>24;
-			return nil;
-		default:
-			return "bug in fswalk1";
-		}
+	if(!strcmp(ctl, "flushauth")){
+		u = nil;
+		if(arg && *arg)
+			u = saneurl(url(arg, 0));
+		flushauth(u, 0);
+		freeurl(u);
+		return nil;
 	}
+	return "bad ctl message";
+}
 
-	i = TYPE(path)+1;
-	for(; i<nelem(tab); i++){
-		if(i==Qclient){
-			n = atoi(name);
-			snprint(buf, sizeof buf, "%d", n);
-			if(n < nclient && strcmp(buf, name) == 0){
-				qid->path = PATH(i, n);
-				qid->type = tab[i].mode>>24;
-				return nil;
+static char*
+clientctl(Client *cl, char *ctl, char *arg)
+{
+	char *p;
+	Url *u;
+	Key *k;
+
+	if(debug)
+		fprint(2, "clientctl: %q %q\n", ctl, arg);
+
+	if(!strcmp(ctl, "url")){
+		if((u = saneurl(url(arg, cl->baseurl))) == nil)
+			return "bad url";
+		freeurl(cl->url);
+		cl->url = u;
+	}
+	else if(!strcmp(ctl, "baseurl")){
+		if((u = url(arg, 0)) == nil)
+			return "bad baseurl";
+		freeurl(cl->baseurl);
+		cl->baseurl = u;
+	}
+	else if(!strcmp(ctl, "request")){
+		p = cl->request;
+		strncpy(p, arg, sizeof(cl->request));
+		for(; *p && isalpha(*p); p++)
+			*p = toupper(*p);
+		*p = 0;
+	}
+	else if(!strcmp(ctl, "headers")){
+		while(arg && *arg){
+			ctl = arg;
+			while(*ctl && strchr("\r\n\t ", *ctl))
+				ctl++;
+			if(arg = strchr(ctl, '\n'))
+				*arg++ = 0;
+			if(k = parsehdr(ctl)){
+				k->next = cl->hdr;
+				cl->hdr = k;
 			}
-			break;
 		}
-		if(i==Qbodyext){
-			ext = client[NUM(path)]->ext;
-			snprint(buf, sizeof buf, "body.%s", ext == nil ? "xxx" : ext);
-			if(strcmp(buf, name) == 0){
-				qid->path = PATH(i, NUM(path));
-				qid->type = tab[i].mode>>24;
-				return nil;
+	}
+	else {
+		char buf[128], **t;
+		static char *tab[] = {
+			"User-Agent",
+			"Content-Type",
+			nil,
+		};
+		for(t = tab; *t; t++){
+			strncpy(buf, *t, sizeof(buf));
+			if(!strcmp(ctl, fshdrname(buf))){
+				cl->hdr = delkey(cl->hdr, *t);
+				if(arg && *arg)
+					cl->hdr = addkey(cl->hdr, *t, arg);
+				break;
 			}
 		}
-		else if(strcmp(name, tab[i].name) == 0){
-			qid->path = PATH(i, NUM(path));
-			qid->type = tab[i].mode>>24;
-			return nil;
-		}
-		if(tab[i].mode&DMDIR)
-			break;
+		if(*t == nil)
+			return "bad ctl message";
 	}
-	return "directory entry not found";
+	return nil;
 }
 
 static void
-fsflush(Req *r)
+fswrite(Req *r)
 {
-	Req *or;
-	int t;
-	Client *c;
-	ulong path;
+	int n;
+	Webfid *f;
+	char *s, *t;
 
-	or=r;
-	while(or->ifcall.type==Tflush)
-		or = or->oldreq;
+	f = r->fid->aux;
+	switch(f->level){
+	case Qrctl:
+	case Qctl:
+		n = r->ofcall.count = r->ifcall.count;
+		s = emalloc(n+1);
+		memmove(s, r->ifcall.data, n);
+		while(n > 0 && strchr("\r\n", s[n-1]))
+			n--;
+		s[n] = 0;
+		t = s;
+		while(*t && strchr("\r\n\t ", *t)==0)
+			t++;
+		while(*t && strchr("\r\n\t ", *t))
+			*t++ = 0;
+		if(f->level == Qctl)
+			t = clientctl(f->client, s, t);
+		else
+			t = rootctl(s, t);
+		free(s);
+		respond(r, t);
+		return;
+	case Qpost:
+		bureq(f->buq, r);
+		return;
+	}
+	respond(r, "not implemented");
+}
 
-	if(or->ifcall.type != Tread && or->ifcall.type != Topen)
-		abort();
+static void
+fsflush(Req *r)
+{
+	Webfid *f;
+	Req *o;
 
-	path = or->fid->qid.path;
-	t = TYPE(path);
-	if(t != Qbody && t != Qbodyext)
-		abort();
-
-	c = client[NUM(path)];
-	sendp(c->creq, r);
-	iointerrupt(c->io);
-	ioflush(c->io);
+	if(o = r->oldreq)
+	if(f = o->fid->aux)
+		buflushreq(f->buq, o);
+	respond(r, nil);
 }
 
 static void
-fsthread(void*)
+fsdestroyfid(Fid *fid)
 {
-	ulong path;
-	Alt a[3];
-	Fid *fid;
-	Req *r;
+	Webfid *f;
 
-	threadsetname("fsthread");
-	plumbstart();
-
-	a[0].op = CHANRCV;
-	a[0].c = cclunk;
-	a[0].v = &fid;
-	a[1].op = CHANRCV;
-	a[1].c = creq;
-	a[1].v = &r;
-	a[2].op = CHANEND;
-
-	for(;;){
-		switch(alt(a)){
-		case 0:
-			path = fid->qid.path;
-			if(TYPE(path)==Qcookies)
-				cookieclunk(fid);
-			if(fid->omode != -1 && TYPE(path) >= Qclient)
-				closeclient(client[NUM(path)]);
-			sendp(cclunkwait, nil);
-			break;
-		case 1:
-			switch(r->ifcall.type){
-			case Tattach:
-				fsattach(r);
-				break;
-			case Topen:
-				fsopen(r);
-				break;
-			case Tread:
-				fsread(r);
-				break;
-			case Twrite:
-				fswrite(r);
-				break;
-			case Tstat:
-				fsstat(r);
-				break;
-			case Tflush:
-				fsflush(r);
-				break;
-			default:
-				respond(r, "bug in fsthread");
-				break;
+	if(f = fid->aux){
+		fid->aux = nil;
+		if(f->buq){
+			buclose(f->buq, 0);
+			if(f->client->qbody == f->buq){
+				f->client->obody = 0;
+				f->client->cbody = 1;
 			}
-			sendp(creqwait, 0);
-			break;
+			bufree(f->buq);
 		}
+		if(f->key)
+			free(f->key);
+		freeclient(f->client);
+		free(f);
 	}
 }
 
-static void
-fssend(Req *r)
+Srv fs = 
 {
-	sendp(creq, r);
-	recvp(creqwait);	/* avoids need to deal with spurious flushes */
-}
+	.attach=fsattach,
+	.stat=fsstat,
+	.walk1=fswalk1,
+	.clone=fsclone,
+	.open=fsopen,
+	.read=fsread,
+	.write=fswrite,
+	.flush=fsflush,
+	.destroyfid=fsdestroyfid,
+};
 
 void
-initfs(void)
+usage(void)
 {
-	time0 = time(0);
-	creq = chancreate(sizeof(void*), 0);
-	creqwait = chancreate(sizeof(void*), 0);
-	cclunk = chancreate(sizeof(void*), 0);
-	cclunkwait = chancreate(sizeof(void*), 0);
-	procrfork(fsthread, nil, STACK, RFNAMEG);
+	fprint(2, "usage: %s [-D] [-m mtpt] [-s srv]\n", argv0);
+	exits("usage");
 }
 
 void
-takedown(Srv*)
+main(int argc, char *argv[])
 {
-	closecookies();
-	threadexitsall("done");
-}
+	char *srv, *mtpt, *s;
 
-Srv fs = 
-{
-.attach=		fssend,
-.destroyfid=	fsdestroyfid,
-.walk1=		fswalk1,
-.open=		fssend,
-.read=		fssend,
-.write=		fssend,
-.stat=		fssend,
-.flush=		fssend,
-.end=		takedown,
-};
+	quotefmtinstall();
+	fmtinstall('U', Ufmt);
+	fmtinstall('E', Efmt);
 
+	srv = nil;
+	mtpt = "/mnt/web";
+	user = getuser();
+	time0 = time(0);
+
+	ARGBEGIN {
+	case 'D':
+		chatty9p++;
+		break;
+	case 'm':
+		mtpt = EARGF(usage());
+		break;
+	case 's':
+		srv = EARGF(usage());
+		break;
+	case 'd':
+		debug++;
+		break;
+	default:
+		usage();
+	} ARGEND;
+
+	rfork(RFNOTEG);
+
+	if(s = getenv("httpproxy")){
+		proxy = saneurl(url(s, 0));
+		free(s);
+	}
+
+	postmountsrv(&fs, srv, mtpt, MREPL);
+}
--- a/sys/src/cmd/webfs/http.c
+++ b/sys/src/cmd/webfs/http.c
@@ -1,540 +1,825 @@
 #include <u.h>
 #include <libc.h>
-#include <bio.h>
-#include <ip.h>
-#include <plumb.h>
-#include <thread.h>
+#include <ctype.h>
 #include <fcall.h>
+#include <thread.h>
 #include <9p.h>
-#include <libsec.h>
-#include <auth.h>
+
 #include "dat.h"
 #include "fns.h"
 
-char PostContentType[] = "application/x-www-form-urlencoded";
-int httpdebug;
+#include <auth.h>
+#include <mp.h>
+#include <libsec.h>
 
-typedef struct HttpState HttpState;
-struct HttpState
+typedef struct Hconn Hconn;
+typedef struct Hpool Hpool;
+typedef struct Hauth Hauth;
+
+struct Hconn
 {
-	int fd;
-	Client *c;
-	char *location;
-	char *setcookie;
-	char *netaddr;
-	char *credentials;
-	char autherror[ERRMAX];
-	Ibuf	b;
+	Hconn	*next;
+
+	int	fd;
+	int	keep;
+	int	cancel;
+	int	len;
+	char	addr[128];
+	char	buf[8192+2];
 };
 
-static void
-location(HttpState *hs, char *value)
+struct Hpool
 {
-	if(hs->location == nil)
-		hs->location = estrdup(value);
-}
+	QLock;
 
-static void
-contenttype(HttpState *hs, char *value)
+	Hconn	*head;
+	int	active;
+	int	limit;
+};
+
+struct Hauth
 {
-	if(hs->c->contenttype != nil)
-		free(hs->c->contenttype);
-	hs->c->contenttype = estrdup(value);
-}
+	Hauth	*next;
+	Url	*url;
+	char	*auth;
+};
 
-static void
-setcookie(HttpState *hs, char *value)
+static Hpool hpool = {
+	.limit	= 16,
+};
+
+static QLock authlk;
+static Hauth *hauth;
+
+static Hconn*
+hdial(Url *u)
 {
-	char *s, *t;
-	Fmt f;
+	char addr[128];
+	Hconn *h, *p;
+	int fd, ofd;
 
-	s = hs->setcookie;
-	fmtstrinit(&f);
-	if(s)
-		fmtprint(&f, "%s", s);
-	fmtprint(&f, "set-cookie: ");
-	fmtprint(&f, "%s", value);
-	fmtprint(&f, "\n");
-	t = fmtstrflush(&f);
-	if(t){
-		free(s);
-		hs->setcookie = t;
+	snprint(addr, sizeof(addr), "tcp!%s!%s", u->host, u->port ? u->port : u->scheme);
+
+	qlock(&hpool);
+	for(p = nil, h = hpool.head; h; p = h, h = h->next){
+		if(strcmp(h->addr, addr) == 0){
+			if(p)
+				p->next = h->next;
+			else
+				hpool.head = h->next;
+			h->next = nil;
+			qunlock(&hpool);
+			return h;
+		}
 	}
+	hpool.active++;
+	qunlock(&hpool);
+	if(debug)
+		fprint(2, "hdial [%d] %s\n", hpool.active, addr);
+
+	if((fd = dial(addr, 0, 0, 0)) < 0)
+		return nil;
+	if(strcmp(u->scheme, "https") == 0){
+		TLSconn *tc;
+
+		tc = emalloc(sizeof(*tc));
+		fd = tlsClient(ofd = fd, tc);
+		close(ofd);
+		/* BUG: should validate but how? */
+		free(tc->cert);
+		free(tc->sessionID);
+		free(tc);
+		if(fd < 0)
+			return nil;
+	}
+
+	h = emalloc(sizeof(*h));
+	h->next = nil;
+	h->cancel = 0;
+	h->keep = 1;
+	h->len = 0;
+	h->fd = fd;
+	strncpy(h->addr, addr, sizeof(h->addr));
+
+	return h;
 }
 
-static char*
-unquote(char *s, char **ps)
+static void
+hclose(Hconn *h)
 {
-	char *p;
+	Hconn *x, *t;
+	int i;
 
-	if(*s != '"'){
-		p = strpbrk(s, " \t\r\n");
-		*p++ = 0;
-		*ps = p;
-		return s;
-	}
-	for(p=s+1; *p; p++){
-		if(*p == '\"'){
-			*p++ = 0;
-			break;
+	if(h == nil)
+		return;
+
+	qlock(&hpool);
+	if(h->keep && h->fd >= 0){
+		for(i = 0, t = nil, x = hpool.head; x; x = x->next){
+			if(strcmp(x->addr, h->addr) == 0)
+				break;
+			if(++i < hpool.limit)
+				t = x;
 		}
-		if(*p == '\\' && *(p+1)){
-			p++;
-			continue;
+		if(x == nil){
+			/* return connection to pool */
+			h->next = hpool.head;
+			hpool.head = h;
+
+			/* cut off tail */
+			if(t){
+				x = t->next;
+				t->next = nil;
+			}
+			qunlock(&hpool);
+
+			/* free the tail */
+			while(h = x){
+				x = h->next;
+				h->next = nil;
+				h->keep = 0;
+				hclose(h);
+			}
+			return;
 		}
 	}
-	memmove(s, s+1, p-(s+1));
-	s[p-(s+1)] = 0;
-	*ps = p;
-	return s;
-}
+	hpool.active--;
+	qunlock(&hpool);
 
-static char*
-servername(char *addr)
-{
-	char *p;
+	if(debug)
+		fprint(2, "hclose [%d] %s\n", hpool.active, h->addr);
 
-	if(strncmp(addr, "tcp!", 4) == 0
-	|| strncmp(addr, "net!", 4) == 0)
-		addr += 4;
-	addr = estrdup(addr);
-	p = addr+strlen(addr);
-	if(p>addr && *(p-1) == 's')
-		p--;
-	if(p>addr+5 && strcmp(p-5, "!http") == 0)
-		p[-5] = 0;
-	return addr;
+	if(h->fd >= 0)
+		close(h->fd);
+	free(h);
 }
 
-void
-wwwauthenticate(HttpState *hs, char *line)
+static int
+hread(Hconn *h, void *data, int len)
 {
-	char cred[64], *user, *pass, *realm, *s, *spec, *name;
-	Fmt fmt;
-	UserPasswd *up;
-
-	spec = nil;
-	up = nil;
-	cred[0] = 0;
-	hs->autherror[0] = 0;
-	if(cistrncmp(line, "basic ", 6) != 0){
-		werrstr("unknown auth: %s", line);
-		goto error;
+	if(h->len > 0){
+		if(len > h->len)
+			len = h->len;
+		memmove(data, h->buf, len);
+		h->len -= len;
+		if(h->len > 0)
+			memmove(h->buf, h->buf + len, h->len);
+		return len;
 	}
-	line += 6;
-	if(cistrncmp(line, "realm=", 6) != 0){
-		werrstr("missing realm: %s", line);
-		goto error;
-	}
-	line += 6;
-	user = hs->c->url->user;
-	pass = hs->c->url->passwd;
-	if(user==nil || pass==nil){
-		realm = unquote(line, &line);
-		fmtstrinit(&fmt);
-		name = servername(hs->netaddr);
-		fmtprint(&fmt, "proto=pass service=http server=%q realm=%q", name, realm);
-		free(name);
-		if(hs->c->url->user)
-			fmtprint(&fmt, " user=%q", hs->c->url->user);
-		spec = fmtstrflush(&fmt);
-		if(spec == nil)
-			goto error;
-		if((up = auth_getuserpasswd(nil, "%s", spec)) == nil)
-			goto error;
-		user = up->user;
-		pass = up->passwd;
-	}
-	if((s = smprint("%s:%s", user, pass)) == nil)
-		goto error;
-	free(up);
-	enc64(cred, sizeof(cred), (uchar*)s, strlen(s));
-	memset(s, 0, strlen(s));
-	free(s);
-	hs->credentials = smprint("Basic %s", cred);
-	if(hs->credentials == nil)
-		goto error;
-	return;
-
-error:
-	free(up);
-	free(spec);
-	snprint(hs->autherror, sizeof hs->autherror, "%r");
-	fprint(2, "%s: Authentication failed: %r\n", argv0);
+	if((len = read(h->fd, data, len)) <= 0)
+		h->keep = 0;
+	return len;
 }
 
-struct {
-	char *name;									/* Case-insensitive */
-	void (*fn)(HttpState *hs, char *value);
-} hdrtab[] = {
-	{ "location:", location },
-	{ "content-type:", contenttype },
-	{ "set-cookie:", setcookie },
-	{ "www-authenticate:", wwwauthenticate },
-};
-
 static int
-httprcode(HttpState *hs)
+hwrite(Hconn *h, void *data, int len)
 {
-	int n;
-	char *p;
-	char buf[256];
-
-	n = readline(&hs->b, buf, sizeof(buf)-1);
-	if(n <= 0)
-		return n;
-	if(httpdebug)
-		fprint(2, "-> %s\n", buf);
-	p = strchr(buf, ' ');
-	if(memcmp(buf, "HTTP/", 5) != 0 || p == nil){
-		werrstr("bad response from server");
+	if(write(h->fd, data, len) != len){
+		h->keep = 0;
 		return -1;
 	}
-	buf[n] = 0;
-	return atoi(p+1);
+	return len;
 }
- 
-/*
- *  read a single mime header, collect continuations.
- *
- *  this routine assumes that there is a blank line twixt
- *  the header and the message body, otherwise bytes will
- *  be lost.
- */
+
 static int
-getheader(HttpState *hs, char *buf, int n)
+hline(Hconn *h, char *data, int len, int cont)
 {
-	char *p, *e;
-	int i;
+	char *x, *y, *e;
+	int n;
 
-	n--;
-	p = buf;
-	for(e = p + n; ; p += i){
-		i = readline(&hs->b, p, e-p);
-		if(i < 0)
-			return i;
-
-		if(p == buf){
-			/* first line */
-			if(strchr(buf, ':') == nil)
-				break;		/* end of headers */
-		} else {
-			/* continuation line */
-			if(*p != ' ' && *p != '\t'){
-				unreadline(&hs->b, p);
-				*p = 0;
-				break;		/* end of this header */
+	data[0] = 0;
+	for(;;){
+		if(h->len > 0){
+			while(x = memchr(h->buf, '\n', h->len)){
+				n = x - h->buf;
+				if(n > 0 && x[-1] == '\r')
+					n--;
+				if(n > 0 && cont){
+					e = h->buf + h->len;
+					for(y = x+1; y < e; y++)
+						if(!strchr("\t ", *y))
+							break;
+					if(y >= e || strchr("\t ", *y))
+						break;
+					if(y > x+1){
+						if(x > h->buf && x[-1] == '\r')
+							x--;
+						memmove(x, y, e - y);
+						h->len -= y - x;
+						continue;
+					}
+				}			
+				if(n < len)
+					len = n;
+				memmove(data, h->buf, len);
+				data[len] = 0;
+				h->len -= (++x - h->buf);
+				if(h->len > 0)
+					memmove(h->buf, x, h->len);
+				return len;
 			}
 		}
+		if(h->len >= sizeof(h->buf))
+			return 0;
+		if((n = read(h->fd, h->buf + h->len, sizeof(h->buf) - h->len)) <= 0){
+			h->keep = 0;
+			return -1;
+		}
+		h->len += n;
 	}
-
-	if(httpdebug)
-		fprint(2, "-> %s\n", buf);
-	return p-buf;
 }
 
 static int
-httpheaders(HttpState *hs)
+authenticate(Url *u, Url *ru, char *method, char *s)
 {
-	char buf[2048];
-	char *p;
-	int i, n;
+	char *user, *pass, *realm, *nonce, *opaque, *x;
+	UserPasswd *up;
+	Hauth *a;
+	Fmt fmt;
+	int n;
 
-	for(;;){
-		n = getheader(hs, buf, sizeof(buf));
-		if(n < 0)
+	up = nil;
+	user = u->user;
+	pass = u->pass;
+	realm = nonce = opaque = nil;
+	fmtstrinit(&fmt);
+	if(!cistrncmp(s, "Basic ", 6)){
+		char cred[64];
+
+		s += 6;
+		if(x = cistrstr(s, "realm="))
+			realm = unquote(x+6, &s);
+		if(realm == nil)
 			return -1;
-		if(n == 0)
-			return 0;
-		//	print("http header: '%.*s'\n", n, buf);
-		for(i = 0; i < nelem(hdrtab); i++){
-			n = strlen(hdrtab[i].name);
-			if(cistrncmp(buf, hdrtab[i].name, n) == 0){
-				/* skip field name and leading white */
-				p = buf + n;
-				while(*p == ' ' || *p == '\t')
-					p++;
-				(*hdrtab[i].fn)(hs, p);
-				break;
-			}
+		if(user == nil || pass == nil){
+			fmtprint(&fmt, " realm=%q", realm);
+			if(user)
+				fmtprint(&fmt, " user=%q", user);
+			if((s = fmtstrflush(&fmt)) == nil)
+				return -1;
+			if((up = auth_getuserpasswd(nil, "proto=pass service=http server=%q%s",
+				u->host, s)) == nil)
+				return -1;
+			user = up->user;
+			pass = up->passwd;
 		}
+		fmtstrinit(&fmt);
+		fmtprint(&fmt, "%s:%s", user ? user : "", pass ? pass : "");
+		free(up);
+		if((s = fmtstrflush(&fmt)) == nil)
+			return -1;
+		n = enc64(cred, sizeof(cred), (uchar*)s, strlen(s));
+		memset(s, 0, strlen(s));
+		free(s);
+		if(n == -1)
+			return -1;
+		fmtstrinit(&fmt);
+		fmtprint(&fmt, "Basic %s", cred);
+		u = saneurl(url(".", u));	/* all uris below the requested one */
+	}else
+	if(!cistrncmp(s, "Digest ", 7)){
+		char chal[1024], ouser[128], resp[2*MD5LEN+1];
+		int nchal;
+
+		s += 7;
+		if(x = cistrstr(s, "realm="))
+			realm = unquote(x+6, &s);
+		if(x = cistrstr(s, "nonce="))
+			nonce = unquote(x+6, &s);
+		if(x = cistrstr(s, "opaque="))
+			opaque = unquote(x+7, &s);
+		if(realm == nil || nonce == nil)
+			return -1;
+		nchal = snprint(chal, sizeof(chal), "%s %s %U", nonce, method, ru);
+		fmtprint(&fmt, " realm=%q", realm);
+		if(user)
+			fmtprint(&fmt, " user=%q", user);
+		if((s = fmtstrflush(&fmt)) == nil)
+			return -1;
+		if(auth_respond(chal, nchal, ouser, sizeof ouser, resp, sizeof resp, nil,
+			"proto=httpdigest role=client server=%q%s", u->host, s) < 0)
+			return -1;
+		fmtstrinit(&fmt);
+		fmtprint(&fmt, "Digest ");
+		fmtprint(&fmt, "username=\"%s\", ", ouser);
+		fmtprint(&fmt, "realm=\"%s\", ", realm);
+		fmtprint(&fmt, "host=\"%s\", ", u->host);
+		fmtprint(&fmt, "uri=\"%U\", ", ru);
+		fmtprint(&fmt, "nonce=\"%s\", ", nonce);
+		fmtprint(&fmt, "response=\"%s\"", resp);
+		if(opaque)
+			fmtprint(&fmt, ", opaque=\"%s\"", opaque);
+		u = saneurl(url("/", u));	/* BUG: should be the ones in domain= only */
+	} else
+		return -1;
+	if(u == nil)
+		return -1;
+	if((s = fmtstrflush(&fmt)) == nil){
+		freeurl(u);
+		return -1;
 	}
+	a = emalloc(sizeof(*a));
+	a->url = u;
+	a->auth = s;
+	qlock(&authlk);
+	a->next = hauth;
+	hauth = a;
+	qunlock(&authlk);
+
+	return 0;
 }
 
-int
-httpopen(Client *c, Url *url)
+void
+flushauth(Url *u, char *t)
 {
-	int fd, code, redirect, authenticate;
-	char *cookies;
-	Ioproc *io;
-	HttpState *hs;
-	char *service;
+	Hauth *a, *p;
 
-	if(httpdebug)
-		fprint(2, "httpopen\n");
-	io = c->io;
-	hs = emalloc(sizeof(*hs));
-	hs->c = c;
+	qlock(&authlk);
+Again:
+	for(p = nil, a = hauth; a; p = a, a = a->next)
+		if(matchurl(u, a->url) && (t == nil || !strcmp(t, a->auth))){
+			if(p)
+				p->next = a->next;
+			else
+				hauth = a->next;
+			if(debug)
+				fprint(2, "flushauth for %U\n", a->url);
+			freeurl(a->url);
+			memset(a->auth, 0, strlen(a->auth));
+			free(a->auth);
+			free(a);
+			goto Again;
+		}
+	qunlock(&authlk);
+}
 
-	if(url->port)
-		service = url->port;
+static void
+catch(void *, char *msg)
+{
+	if(strstr("alarm", msg) || strstr("die", msg))
+		noted(NCONT);
 	else
-		service = url->scheme;
-	hs->netaddr = estrdup(netmkaddr(url->host, 0, service));
-	c->aux = hs;
-	if(httpdebug){
-		fprint(2, "dial %s\n", hs->netaddr);
-		fprint(2, "dial port: %s\n", url->port);
-	}
-	fd = iotlsdial(io, hs->netaddr, 0, 0, 0, url->ischeme==UShttps);
-	if(fd < 0){
-	Error:
-		if(httpdebug)
-			fprint(2, "iodial: %r\n");
-		free(hs->location);
-		free(hs->setcookie);
-		free(hs->netaddr);
-		free(hs->credentials);
-		if(fd >= 0)
-			ioclose(io, hs->fd);
-		hs->fd = -1;
-		free(hs);
-		c->aux = nil;
-		return -1;
-	}
-	hs->fd = fd;
-	if(httpdebug)
-		fprint(2, "<- %s %s HTTP/1.0\n<- Host: %s\n",
-			c->havepostbody? "POST": "GET", url->http.page_spec, url->host);
-	ioprint(io, fd, "%s %s HTTP/1.0\r\nHost: %s\r\n",
-		c->havepostbody? "POST" : "GET", url->http.page_spec, url->host);
-	if(httpdebug)
-		fprint(2, "<- User-Agent: %s\n", c->ctl.useragent);
-	if(c->ctl.useragent)
-		ioprint(io, fd, "User-Agent: %s\r\n", c->ctl.useragent);
-	if(c->ctl.sendcookies){
-		/* should we use url->page here?  sometimes it is nil. */
-		cookies = httpcookies(url->host, url->http.page_spec,
-			url->ischeme == UShttps);
-		if(cookies && cookies[0])
-			ioprint(io, fd, "%s", cookies);
-		if(httpdebug)
-			fprint(2, "<- %s", cookies);
-		free(cookies);
-	}
-	if(c->havepostbody){
-		ioprint(io, fd, "Content-type: %s\r\n", PostContentType);
-		ioprint(io, fd, "Content-length: %ud\r\n", c->npostbody);
-		if(httpdebug){
-			fprint(2, "<- Content-type: %s\n", PostContentType);
-			fprint(2, "<- Content-length: %ud\n", c->npostbody);
+		noted(NDFLT);
+}
+
+#define NOLENGTH 0x7fffffffffffffffLL
+
+void
+http(char *m, Url *u, Key *shdr, Buq *qbody, Buq *qpost)
+{
+	int i, l, n, try, pid, fd, cfd, chunked, retry, nobody;
+	char *s, *x, buf[8192+2], status[256], method[16];
+	vlong length, offset;
+	Url ru, tu, *nu;
+	Key *k, *rhdr;
+	Hconn *h;
+	Hauth *a;
+
+	incref(qbody);
+	if(qpost) incref(qpost);
+	strncpy(method, m, sizeof(method));
+	switch(rfork(RFPROC|RFMEM|RFNOWAIT)){
+	default:
+		return;
+	case -1:
+		buclose(qbody, "can't fork");
+		bufree(qbody);
+		buclose(qpost, "can't fork");
+		bufree(qpost);
+		while(k = shdr){
+			shdr = k->next;
+			free(k);
 		}
+		freeurl(u);
+		return;
+	case 0:
+		break;
 	}
-	if(c->authenticate){
-		ioprint(io, fd, "Authorization: %s\r\n", c->authenticate);
-		if(httpdebug)
-			fprint(2, "<- Authorization: %s\n", c->authenticate);
-	}
-	ioprint(io, fd, "\r\n");
-	if(c->havepostbody)
-		if(iowrite(io, fd, c->postbody, c->npostbody) != c->npostbody)
-			goto Error;
 
-	redirect = 0;
-	authenticate = 0;
-	initibuf(&hs->b, io, fd);
-	code = httprcode(hs);
+	notify(catch);
+	if(qpost){
+		/* file for spooling the postbody if we need to restart the request */
+		snprint(buf, sizeof(buf), "/tmp/http.%d.%d.post", getppid(), getpid());
+		fd = create(buf, OEXCL|ORDWR|ORCLOSE, 0600);
+	} else
+		fd = -1;
 
-	switch(code){
-	case -1:	/* connection timed out */
-		goto Error;
+	h = nil;
+	pid = 0;
+	werrstr("too many errors");
+	for(try = 0; try < 6; try++){
+		if(u == nil || (strcmp(u->scheme, "http") && strcmp(u->scheme, "https"))){
+			werrstr("bad url");
+			break;
+		}
 
-/*
-	case Eof:
-		werrstr("EOF from HTTP server");
-		goto Error;
-*/
+		if(debug)
+			fprint(2, "http(%d): %s %U\n", try, method, u);
 
-	case 200:	/* OK */
-	case 201:	/* Created */
-	case 202:	/* Accepted */
-	case 204:	/* No Content */
-	case 205: /* Reset Content */
-#ifdef NOT_DEFINED
-		if(ofile == nil && r->start != 0)
-			sysfatal("page changed underfoot");
-#endif
-		break;
+		/* preemptive authentication from hauth cache */
+		qlock(&authlk);
+		if(proxy && !lookkey(shdr, "Proxy-Authorization"))
+			for(a = hauth; a; a = a->next)
+				if(matchurl(a->url, proxy)){
+					shdr = addkey(shdr, "Proxy-Authorization", a->auth);
+					break;
+				}
+		if(!lookkey(shdr, "Authorization"))
+			for(a = hauth; a; a = a->next)
+				if(matchurl(a->url, u)){
+					shdr = addkey(shdr, "Authorization", a->auth);
+					break;
+				}
+		qunlock(&authlk);
 
-	case 206:	/* Partial Content */
-		werrstr("Partial Content (206)");
-		goto Error;
+		if(proxy){
+			ru = *u;
+			ru.fragment = nil;
+		} else {
+			memset(&ru, 0, sizeof(tu));
+			ru.path = Upath(u);
+			ru.query = u->query;
+		}
+		n = snprint(buf, sizeof(buf), "%s %U HTTP/1.1\r\nHost: %s%s%s\r\n",
+			method, &ru, u->host, u->port ? ":" : "", u->port ? u->port : "");
 
-	case 303:	/* See Other */
-		c->havepostbody = 0;
-	case 301:	/* Moved Permanently */
-	case 302:	/* Moved Temporarily */
-	case 307: /* Temporary Redirect  */
-		redirect = 1;
-		break;
+		for(k = shdr; k; k = k->next)
+			n += snprint(buf+n, sizeof(buf)-2 - n, "%s: %s\r\n", k->key, k->val);
 
-	case 304:	/* Not Modified */
-		break;
+		if(n >= sizeof(buf)-64){
+			werrstr("request too large");
+			break;
+		}
 
-	case 400:	/* Bad Request */
-		werrstr("Bad Request (400)");
-		goto Error;
+		nobody = !cistrcmp(method, "HEAD");
+		length = 0;
+		chunked = 0;
+		if(qpost){
+			qlock(qpost);
+			/* wait until buffer is full, most posts are small */
+			while(!qpost->closed && qpost->size < qpost->limit)
+				rsleep(&qpost->rz);
 
-	case 401:	/* Unauthorized */
-		if(c->authenticate){
-			werrstr("Authentication failed (401)");
-			goto Error;
+			if(lookkey(shdr, "Content-Length"))
+				chunked = 0;
+			else if(x = lookkey(shdr, "Transfer-Encoding"))
+				chunked = cistrstr(x, "chunked") != nil;
+			else if(chunked = !qpost->closed)
+				n += snprint(buf+n, sizeof(buf)-n, "Transfer-Encoding: chunked\r\n");
+			else if(qpost->closed){
+				if(fd >= 0){
+					length = seek(fd, 0, 2);
+					if(length < 0)
+						length = 0;
+				}
+				length += qpost->size;
+				n += snprint(buf+n, sizeof(buf)-n, "Content-Length: %lld\r\n", length);
+			}
+			qunlock(qpost);
 		}
-		authenticate = 1;
-		break;
-	case 402:	/* Payment Required */
-		werrstr("Payment Required (402)");
-		goto Error;
 
-	case 403:	/* Forbidden */
-		werrstr("Forbidden by server (403)");
-		goto Error;
+		/* give 5 seconds to dial */
+		if(h == nil){
+			alarm(5000);
+			if((h = hdial(proxy ? proxy : u)) == nil)
+				break;
+		}
 
-	case 404:	/* Not Found */
-		werrstr("Not found on server (404)");
-		goto Error;
+		if((cfd = open("/mnt/webcookies/http", ORDWR)) >= 0){
+			/* only scheme, host and path are relevant for cookies */
+			memset(&tu, 0, sizeof(tu));
+			tu.scheme = u->scheme;
+			tu.host = u->host;
+			tu.path = Upath(u);
+			fprint(cfd, "%U", &tu);
+			for(;;){
+				if(n >= sizeof(buf)-2){
+					if(debug)
+						fprint(2, "-> %.*s", n, buf);
+					if(hwrite(h, buf, n) != n)
+						goto Badflush;
+					n = 0;
+				}
+				if((l = read(cfd, buf+n, sizeof(buf)-2 - n)) == 0)
+					break;
+				if(l < 0){
+					close(cfd);
+					cfd = -1;
+					break;
+				}
+				n += l;
+			}
+		}
 
-	case 405:	/* Method Not Allowed  */
-		werrstr("Method not allowed (405)");
-		goto Error;
+		n += snprint(buf+n, sizeof(buf)-n, "\r\n");
+		if(debug)
+			fprint(2, "-> %.*s", n, buf);
+		if(hwrite(h, buf, n) != n){
+		Badflush:
+			alarm(0);
+			goto Retry;
+		}
 
-	case 406: /* Not Acceptable */
-		werrstr("Not Acceptable (406)");
-		goto Error;
+		if(qpost){
+			h->cancel = 0;
+			if((pid = rfork(RFMEM|RFPROC)) <= 0){
+				int ifd;
 
-	case 407:	/* Proxy auth */
-		werrstr("Proxy authentication required (407)");
-		goto Error;
+				alarm(0);
+				if((ifd = fd) >= 0)
+					seek(ifd, 0, 0);
+				while(!h->cancel){
+					if((ifd < 0) || ((n = read(ifd, buf, sizeof(buf)-2)) <= 0)){
+						ifd = -1;
+						if((n = buread(qpost, buf, sizeof(buf)-2)) <= 0)
+							break;
+						if(fd >= 0)
+							if(write(fd, buf, n) != n)
+								break;
+					}
+					if(chunked){
+						char tmp[32];
+						hwrite(h, tmp, snprint(tmp, sizeof(tmp), "%x\r\n", n));
+						buf[n++] = '\r';
+						buf[n++] = '\n';
+					}
+					if(hwrite(h, buf, n) != n)
+						break;
+				}
+				if(chunked)
+					hwrite(h, "0\r\n\r\n", 5);
+				else
+					h->keep = 0;
+				if(pid == 0)
+					exits(0);
+			}
+			/* no timeout when posting */
+			alarm(0);
+		} else {
+			/* wait 10 seconds for the response */
+			alarm(10000);
+		}
 
-	case 408: /* Request Timeout */
-		werrstr("Request Timeout (408)");
-		goto Error;
+		Cont:
+		rhdr = 0;
+		retry = 0;
+		chunked = 0;
+		status[0] = 0;
+		offset = 0;
+		length = NOLENGTH;
+		for(l = 0; hline(h, s = buf, sizeof(buf)-1, 1) > 0; l++){
+			if(debug)
+				fprint(2, "<- %s\n", s);
+			if(l == 0){
+				if(x = strchr(s, ' '))
+					while(*x == ' ')
+						*x++ = 0;
+				if(cistrncmp(s, "HTTP", 4)){
+					h->keep = 0;
+					if(cistrcmp(s, "ICY"))
+						break;
+				}
+				strncpy(status, x, sizeof(status));
+				continue;
+			}
+			if((k = parsehdr(s)) == nil)
+				continue;
+			if(!cistrcmp(k->key, "Connection")){
+				if(cistrstr(k->val, "close"))
+					h->keep = 0;
+			}
+			else if(!cistrcmp(k->key, "Content-Length"))
+				length = atoll(k->val);
+			else if(!cistrcmp(k->key, "Transfer-Encoding")){
+				if(cistrstr(k->val, "chunked"))
+					chunked = 1;
+			}
+			else if(!cistrcmp(k->key, "Set-Cookie") || 
+				!cistrcmp(k->key, "Set-Cookie2")){
+				if(cfd >= 0)
+					fprint(cfd, "Set-Cookie: %s\n", k->val);
+				free(k);
+				continue;
+			}
+			k->next = rhdr;
+			rhdr = k;
+		}
+		alarm(0);
+		if(cfd >= 0){
+			close(cfd);
+			cfd = -1;
+		}
 
-	case 409: /* Conflict */
-		werrstr("Conflict  (409)");
-		goto Error;
-	
-	case 410: /* Gone */
-		werrstr("Gone  (410)");
-		goto Error;
-	
-	case 411: /* Length Required */
-		werrstr("Length Required  (411)");
-		goto Error;
-	
-	case 412: /* Precondition Failed */
-		werrstr("Precondition Failed  (412)");
-		goto Error;
-	
-	case 413: /* Request Entity Too Large */
-		werrstr("Request Entity Too Large  (413)");
-		goto Error;
-	
-	case 414: /* Request-URI Too Long */
-		werrstr("Request-URI Too Long  (414)");
-		goto Error;
-	
-	case 415: /* Unsupported Media Type */
-		werrstr("Unsupported Media Type  (415)");
-		goto Error;
-	
-	case 416: /* Requested Range Not Satisfiable */
-		werrstr("Requested Range Not Satisfiable  (416)");
-		goto Error;
-	
-	case 417: /* Expectation Failed */
-		werrstr("Expectation Failed  (417)");
-		goto Error;
+		if((i = atoi(status)) < 0)
+			i = 0;
+		Status:
+		switch(i){
+		default:
+			if(i % 100){
+				i -= (i % 100);
+				goto Status;
+			}
+		case 100:	/* Continue */
+		case 101:	/* Switching Protocols */
+			while(k = rhdr){
+				rhdr = k->next;
+				free(k);
+			}
+			goto Cont;
+		case 304:	/* Not Modified */
+			nobody = 1;
+		case 305:	/* Use Proxy */
+		case 400:	/* Bad Request */
+		case 402:	/* Payment Required */
+		case 403:	/* Forbidden */
+		case 404:	/* Not Found */
+		case 405:	/* Method Not Allowed */
+		case 406:	/* Not Acceptable */
+		case 408:	/* Request Timeout */
+		case 409:	/* Conflict */
+		case 410:	/* Gone */
+		case 411:	/* Length Required */
+		case 412:	/* Precondition Failed */
+		case 413:	/* Request Entity Too Large */
+		case 414:	/* Request URI Too Large */
+		case 415:	/* Unsupported Media Type */
+		case 416:	/* Requested Range Not Satisfiable */
+		case 417:	/* Expectation Failed */
+		case 500:	/* Internal server error */
+		case 501:	/* Not implemented */
+		case 502:	/* Bad gateway */
+		case 503:	/* Service unavailable */
+		case 504:	/* Gateway Timeout */
+		case 505:	/* HTTP Version not Supported */
+		Error:
+			h->cancel = 1;
+			buclose(qbody, status);
+			buclose(qpost, status);
+			break;
+		case 300:	/* Multiple choices */
+		case 302:	/* Found */
+		case 303:	/* See Other */
+			if(qpost){
+				if(pid > 0){
+					waitpid();
+					pid = 0;
+				}
+				buclose(qpost, 0);
+				bufree(qpost);
+				qpost = nil;
+			}
+			if(cistrcmp(method, "HEAD"))
+				strncpy(method, "GET", sizeof(method));
+		case 301:	/* Moved Permanently */
+		case 307:	/* Temporary Redirect */
+		case 308:	/* Resume Incomplete */
+			if((x = lookkey(rhdr, "Location")) == nil)
+				goto Error;
+			if((nu = saneurl(url(x, u))) == nil)
+				goto Error;
+			freeurl(u);
+			u = nu;
+			if(0){
+		case 401:	/* Unauthorized */
+			if(x = lookkey(shdr, "Authorization"))
+				flushauth(nil, x);
+			if((x = lookkey(rhdr, "WWW-Authenticate")) == nil)
+				goto Error;
+			if(authenticate(u, &ru, method, x) < 0)
+				goto Error;
+			}
+			if(0){
+		case 407:	/* Proxy Auth */
+			if(proxy == nil)
+				goto Error;
+			if(x = lookkey(shdr, "Proxy-Authorization"))
+				flushauth(proxy, x);
+			if((x = lookkey(rhdr, "Proxy-Authenticate")) == nil)
+				goto Error;
+			if(authenticate(proxy, proxy, method, x) < 0)
+				goto Error;
+			}
+		case 0:		/* No status */
+			if(qpost && fd < 0){
+				if(i > 0)
+					goto Error;
+				break;
+			}
+			h->cancel = 1;
+			retry = 1;
+			break;
+		case 204:	/* No Content */
+		case 205:	/* Reset Content */
+			nobody = 1;
+		case 200:	/* OK */
+		case 201:	/* Created */
+		case 202:	/* Accepted */
+		case 203:	/* Non-Authoritative Information */
+		case 206:	/* Partial Content */
+			qbody->url = u; u = nil;
+			qbody->hdr = rhdr; rhdr = nil;
+			if(nobody)
+				buclose(qbody, 0);
+			break;
+		}
 
-	case 500:	/* Internal server error */
-		werrstr("Server choked (500)");
-		goto Error;
+		while(k = rhdr){
+			rhdr = k->next;
+			free(k);
+		}
 
-	case 501:	/* Not implemented */
-		werrstr("Server can't do it (501)");
-		goto Error;
+		/*
+		 * remove authorization headers so on the next round, we use
+		 * the hauth cache (wich checks the scope url). this makes
+		 * sure we wont send credentials to the wrong url after
+		 * a redirect.
+		 */
+		shdr = delkey(shdr, "Proxy-Authorization");
+		shdr = delkey(shdr, "Authorization");
 
-	case 502:	/* Bad gateway */
-		werrstr("Bad gateway (502)");
-		goto Error;
+		if(!chunked && length == NOLENGTH)
+			h->keep = 0;
 
-	case 503:	/* Service unavailable */
-		werrstr("Service unavailable (503)");
-		goto Error;
-	
-	default:
-		/* Bogus: we should treat unknown code XYZ as code X00 */
-		werrstr("Unknown response code %d", code);
-		goto Error;
-	}
+		/*
+		 * read the response body (if any). retry means we'r just
+		 * skipping the error page so we wont touch qbody.
+		 */
+		while(!nobody){
+			if((qbody->closed || retry) && !h->keep)
+				break;
+			if(chunked){
+				if(hline(h, buf, sizeof(buf)-1, 0) <= 0)
+					break;
+				length = strtoll(buf, nil, 16);
+				offset = 0;
+			}
+			while(offset < length){
+				l = sizeof(buf);
+				if(l > (length - offset))
+					l = (length - offset);
+				if((n = hread(h, buf, l)) <= 0)
+					break;
+				offset += n;
+				if(!retry)
+					if(buwrite(qbody, buf, n) != n)
+						break;
+			}
+			if(offset != length){
+				h->keep = 0;
+				if(length != NOLENGTH)
+					break;
+			}
+			if(chunked){
+				while(hline(h, buf, sizeof(buf)-1, 1) > 0){
+					if(debug)
+						fprint(2, "<= %s\n", buf);
+					if(!retry)
+						if(k = parsehdr(buf)){
+							k->next = qbody->hdr;
+							qbody->hdr = k;
+						}
+				}
+				if(length > 0)
+					continue;
+			}
+			if(!retry)
+				buclose(qbody, 0);
+			break;
+		}
 
-	if(httpheaders(hs) < 0)
-		goto Error;
-	if(c->ctl.acceptcookies && hs->setcookie)
-		httpsetcookie(hs->setcookie, url->host, url->path);
-	if(authenticate){
-		if(!hs->credentials){
-			if(hs->autherror[0])
-				werrstr("%s", hs->autherror);
-			else
-				werrstr("unauthorized; no www-authenticate: header");
-			goto Error;
+		if(!retry)
+			break;
+		Retry:
+		if(cfd >= 0)
+			close(cfd);
+		if(pid > 0){
+			waitpid();
+			pid = 0;
 		}
-		c->authenticate = hs->credentials;
-		hs->credentials = nil;
-	}else if(c->authenticate)
-		c->authenticate = 0;
-	if(redirect){
-		if(!hs->location){
-			werrstr("redirection without Location: header");
-			goto Error;
-		}
-		c->redirect = hs->location;
-		hs->location = nil;
+		hclose(h);
+		h = nil;
 	}
-	return 0;
-}
+	alarm(0);
 
-int
-httpread(Client *c, Req *r)
-{
-	HttpState *hs;
-	long n;
+	rerrstr(buf, sizeof(buf));
+	buclose(qbody, buf);
+	bufree(qbody);
 
-	hs = c->aux;
-	n = readibuf(&hs->b, r->ofcall.data, r->ifcall.count);
-	if(n < 0)
-		return -1;
+	if(qpost){
+		if(pid > 0)
+			waitpid();
+		buclose(qpost, buf);
+		bufree(qpost);
+	}
+	if(fd >= 0)
+		close(fd);
 
-	r->ofcall.count = n;
-	return 0;
-}
+	hclose(h);
+	freeurl(u);
 
-void
-httpclose(Client *c)
-{
-	HttpState *hs;
-
-	hs = c->aux;
-	if(hs == nil)
-		return;
-	if(hs->fd >= 0)
-		ioclose(c->io, hs->fd);
-	hs->fd = -1;
-	free(hs->location);
-	free(hs->setcookie);
-	free(hs->netaddr);
-	free(hs->credentials);
-	free(hs);
-	c->aux = nil;
+	while(k = shdr){
+		shdr = k->next;
+		free(k);
+	}
+	exits(0);
 }
--- a/sys/src/cmd/webfs/io.c
+++ /dev/null
@@ -1,86 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <ip.h>
-#include <plumb.h>
-#include <thread.h>
-#include <fcall.h>
-#include <9p.h>
-#include <mp.h>
-#include <libsec.h>
-#include "dat.h"
-#include "fns.h"
-
-static long
-_iovfprint(va_list *arg)
-{
-	int fd;
-	char *fmt;
-	va_list arg2;
-
-	fd = va_arg(*arg, int);
-	fmt = va_arg(*arg, char*);
-	arg2 = va_arg(*arg, va_list);
-	return vfprint(fd, fmt, arg2);
-}
-
-int
-iovfprint(Ioproc *io, int fd, char *fmt, va_list arg)
-{
-	return iocall(io, _iovfprint, fd, fmt, arg);
-}
-
-int
-ioprint(Ioproc *io, int fd, char *fmt, ...)
-{
-	int n;
-	va_list arg;
-
-	va_start(arg, fmt);
-	n = iovfprint(io, fd, fmt, arg);
-	va_end(arg);
-	return n;
-}
-
-static long
-_iotlsdial(va_list *arg)
-{
-	char *addr, *local, *dir;
-	int *cfdp, fd, tfd, usetls;
-	TLSconn conn;
-
-	addr = va_arg(*arg, char*);
-	local = va_arg(*arg, char*);
-	dir = va_arg(*arg, char*);
-	cfdp = va_arg(*arg, int*);
-	usetls = va_arg(*arg, int);
-
-	fd = dial(addr, local, dir, cfdp);
-	if(fd < 0)
-		return -1;
-	if(!usetls)
-		return fd;
-
-	memset(&conn, 0, sizeof conn);
-	/* does no good, so far anyway */
-	// conn.chain = readcertchain("/sys/lib/ssl/vsignss.pem");
-
-	tfd = tlsClient(fd, &conn);
-	close(fd);
-	if(tfd < 0)
-		fprint(2, "%s: tlsClient: %r\n", argv0);
-	else {
-		/* BUG: check cert here? */
-		if(conn.cert)
-			free(conn.cert);
-		if(conn.sessionID)
-			free(conn.sessionID);
-	}
-	return tfd;
-}
-
-int
-iotlsdial(Ioproc *io, char *addr, char *local, char *dir, int *cfdp, int usetls)
-{
-	return iocall(io, _iotlsdial, addr, local, dir, cfdp, usetls);
-}
--- a/sys/src/cmd/webfs/main.c
+++ /dev/null
@@ -1,67 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <ip.h>
-#include <plumb.h>
-#include <thread.h>
-#include <fcall.h>
-#include <9p.h>
-#include "dat.h"
-#include "fns.h"
-
-char *cookiefile;
-char *mtpt = "/mnt/web";
-char *service;
-
-Ctl globalctl = 
-{
-	1,	/* accept cookies */
-	1,	/* send cookies */
-	10,	/* redirect limit */
-	"webfs/2.0 (plan 9)"	/* user agent */
-};
-
-void
-usage(void)
-{
-	fprint(2, "usage: webfs [-c cookies] [-m mtpt] [-s service]\n");
-	threadexitsall("usage");
-}
-
-#include <pool.h>
-void
-threadmain(int argc, char **argv)
-{
-	rfork(RFNOTEG);
-	ARGBEGIN{
-	case 'd':
-		mainmem->flags |= POOL_PARANOIA|POOL_ANTAGONISM;
-		break;
-	case 'D':
-		chatty9p++;
-		break;
-	case 'c':
-		cookiefile = EARGF(usage());
-		break;
-	case 'm':
-		mtpt = EARGF(usage());
-		break;
-	case 's':
-		service = EARGF(usage());
-		break;
-	default:
-		usage();
-	}ARGEND
-
-	quotefmtinstall();
-	if(argc != 0)
-		usage();
-
-	plumbinit();
-	globalctl.useragent = estrdup(globalctl.useragent);
-	initcookies(cookiefile);
-	initurl();
-	initfs();
-	threadpostmountsrv(&fs, service, mtpt, MREPL);
-	threadexits(nil);
-}
--- a/sys/src/cmd/webfs/mkfile
+++ b/sys/src/cmd/webfs/mkfile
@@ -1,35 +1,8 @@
 </$objtype/mkfile
 BIN=/$objtype/bin
-
 TARG=webfs
 
-SCHEMEOFILES=\
-	file.$O\
-	ftp.$O\
-	http.$O\
+HFILES=fns.h dat.h
+OFILES=sub.$O url.$O buq.$O http.$O fs.$O
 
-OFILES=\
-	buf.$O\
-	client.$O\
-	cookies.$O\
-	fs.$O\
-	http.$O\
-	io.$O\
-	main.$O\
-	plumb.$O\
-	url.$O\
-	util.$O\
-#	$SCHEMEOFILES
-
-HFILES=\
-	dat.h\
-	fns.h\
-
-UPDATE=\
-	mkfile\
-	$HFILES\
-	${OFILES:%.$O=%.c}\
-	${TARG:%=/386/bin/%}\
-
 </sys/src/cmd/mkone
-
--- a/sys/src/cmd/webfs/plumb.c
+++ /dev/null
@@ -1,165 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <auth.h>
-#include <fcall.h>
-#include <thread.h>
-#include <plumb.h>
-#include <9p.h>
-
-#include "dat.h"
-#include "fns.h"
-
-static int		plumbsendfd;
-static int		plumbwebfd;
-static Channel	*plumbchan;
-
-static void	plumbwebproc(void*);
-static void	plumbwebthread(void*);
-static void plumbsendproc(void*);
-
-void
-plumbinit(void)
-{
-	plumbsendfd = plumbopen("send", OWRITE|OCEXEC);
-	plumbwebfd = plumbopen("web", OREAD|OCEXEC);
-}
-
-void
-plumbstart(void)
-{
-	plumbchan = chancreate(sizeof(Plumbmsg*), 0);
-	proccreate(plumbwebproc, nil, STACK);
-	threadcreate(plumbwebthread, nil, STACK);
-}
-
-static void
-plumbwebthread(void*)
-{
-	char *base;
-	Plumbmsg *m;
-
-	for(;;){
-		m = recvp(plumbchan);
-		if(m == nil)
-			threadexits(nil);
-		base = plumblookup(m->attr, "baseurl");
-		if(base == nil)
-			base = m->wdir;
-		plumburl(m->data, base);
-		plumbfree(m);
-	}
-}
-
-static void
-plumbwebproc(void*)
-{
-	Plumbmsg *m;
-
-	for(;;){
-		m = plumbrecv(plumbwebfd);
-		sendp(plumbchan, m);
-		if(m == nil)
-			threadexits(nil);
-	}
-}
-
-static void
-addattr(Plumbmsg *m, char *name, char *value)
-{
-	Plumbattr *a;
-
-	a = malloc(sizeof(Plumbattr));
-	a->name = name;
-	a->value = value;
-	a->next = m->attr;
-	m->attr = a;
-}
-
-static void
-freeattrs(Plumbmsg *m)
-{
-	Plumbattr *a, *next;
-
-	a = m->attr;
-	while(a != nil) {
-		next = a->next;
-		free(a);
-		a = next;
-	}
-}
-
-static struct
-{
-	char	*ctype;
-	char	*ext;
-}
-ctypes[] =
-{
-	{ "application/msword", "doc" },
-	{ "application/pdf", "pdf" },
-	{ "application/postscript", "ps" },
-	{ "application/rtf", "rtf" },
-	{ "image/gif", "gif" },
-	{ "image/jpeg", "jpg" },
-	{ "image/png", "png" },
-	{ "image/ppm", "ppm" },
-	{ "image/tiff", "tiff" },
-	{ "text/html", "html" },
-	{ "text/plain", "txt" },
-	{ "text/xml", "xml" },
-};
-
-void
-replumb(Client *c)
-{
-	int i;
-	Plumbmsg *m;
-	char name[128], *ctype, *ext, *p;
-
-	if(!c->plumbed)
-		return;
-	m = emalloc(sizeof(Plumbmsg));
-	m->src = "webfs";
-	m->dst = nil;
-	m->wdir = "/";
-	m->type = "text";
-	m->attr = nil;
-	addattr(m, "url", c->url->url);
-	ctype = c->contenttype;
-	ext = nil;
-	if(ctype != nil) {
-		addattr(m, "content-type", ctype);
-		for(i = 0; i < nelem(ctypes); i++) {
-			if(strcmp(ctype, ctypes[i].ctype) == 0) {
-				ext = ctypes[i].ext;
-				break;
-			}
-		}
-	}
-	if(ext == nil) {
-		p = strrchr(c->url->url, '/');
-		if(p != nil)
-			p = strrchr(p+1, '.');
-		if(p != nil && strlen(p) <= 5)
-			ext = p+1;
-		else
-			ext = "txt";		/* punt */
-	}
-	c->ext = ext;
-if(0)fprint(2, "content type %s -> extension .%s\n", ctype, ext);
-	m->ndata = snprint(name, sizeof name, "/mnt/web/%d/body.%s", c->num, ext);
-	m->data = estrdup(name);
-	proccreate(plumbsendproc, m, STACK);	/* separate proc to avoid a deadlock */
-}
-
-static void
-plumbsendproc(void *x)
-{
-	Plumbmsg *m;
-
-	m = x;
-	plumbsend(plumbsendfd, m);
-	freeattrs(m);
-	free(m->data);
-	free(m);
-}
--- /dev/null
+++ b/sys/src/cmd/webfs/sub.c
@@ -1,0 +1,119 @@
+#include <u.h>
+#include <libc.h>
+#include <ctype.h>
+#include <fcall.h>
+#include <thread.h>
+#include <9p.h>
+
+#include "dat.h"
+#include "fns.h"
+
+void*
+emalloc(int n)
+{
+	void *v;
+	v = emalloc9p(n);
+	setmalloctag(v, getcallerpc(&n));
+	memset(v, 0, n);
+	return v;
+}
+
+char*
+estrdup(char *s)
+{
+	s = estrdup9p(s);
+	setmalloctag(s, getcallerpc(&s));
+	return s;
+}
+
+Key*
+addkey(Key *h, char *key, char *val)
+{
+	Key *k;
+	int n;
+
+	if(val == nil)
+		val = "";
+	n = strlen(key)+1;
+	k = emalloc(sizeof(*k) + n + strlen(val)+1);
+	k->next = h;
+	k->val = k->key + n;
+	strcpy(k->key, key);
+	strcpy(k->val, val);
+	return k;
+}
+
+Key*
+delkey(Key *h, char *key)
+{
+	Key *k, *p;
+
+	for(p = nil, k = h; k; p = k, k = k->next){
+		if(!cistrcmp(k->key, key)){
+			if(p)
+				p->next = k->next;
+			else
+				h = k->next;
+			memset(k->val, 0, strlen(k->val));
+			free(k);
+			break;
+		}
+	}
+	return h;
+}
+
+char*
+lookkey(Key *k, char *key)
+{
+	while(k){
+		if(!cistrcmp(k->key, key))
+			return k->val;
+		k = k->next;
+	}
+	return nil;
+}
+
+Key*
+parsehdr(char *s)
+{
+	char *v;
+
+	v = strchr(s, 0)-1;
+	while(v >= s && strchr("\n\r\t ", *v))
+		*v-- = 0;
+	if(v = strchr(s, ':')){
+		*v++ = 0;
+		while(strchr("\t ", *v))
+			v++;
+		if(*s && *v)
+			return addkey(0, s, v);
+	}
+	return nil;
+}
+
+char*
+unquote(char *s, char **ps)
+{
+	char *p;
+
+	if(*s != '"'){
+		p = strpbrk(s, " \t\r\n");
+		*p++ = 0;
+		*ps = p;
+		return s;
+	}
+	for(p=s+1; *p; p++){
+		if(*p == '\"'){
+			*p++ = 0;
+			break;
+		}
+		if(*p == '\\' && *(p+1)){
+			p++;
+			continue;
+		}
+	}
+	memmove(s, s+1, p-(s+1));
+	s[p-(s+1)] = 0;
+	*ps = p;
+	return s;
+}
--- a/sys/src/cmd/webfs/url.c
+++ b/sys/src/cmd/webfs/url.c
@@ -1,871 +1,360 @@
-/*
- * This is a URL parser, written to parse "Common Internet Scheme" URL
- * syntax as described in RFC1738 and updated by RFC2396.  Only absolute URLs 
- * are supported, using "server-based" naming authorities in the schemes.
- * Support for literal IPv6 addresses is included, per RFC2732.
- *
- * Current "known" schemes: http, ftp, file.
- *
- * We can do all the parsing operations without Runes since URLs are
- * defined to be composed of US-ASCII printable characters.
- * See RFC1738, RFC2396.
- */
-
 #include <u.h>
 #include <libc.h>
 #include <ctype.h>
-#include <regexp.h>
-#include <plumb.h>
-#include <thread.h>
 #include <fcall.h>
+#include <thread.h>
 #include <9p.h>
+
 #include "dat.h"
 #include "fns.h"
 
-int urldebug;
-
-/* If set, relative paths with leading ".." segments will have them trimmed */
-#define RemoveExtraRelDotDots	0
-#define ExpandCurrentDocUrls	1
-
-static char*
-schemestrtab[] =
-{
-	nil,
-	"http",
-	"https",
-	"ftp",
-	"file",
-};
-
 static int
-ischeme(char *s)
+dhex(char c)
 {
-	int i;
-
-	for(i=0; i<nelem(schemestrtab); i++)
-		if(schemestrtab[i] && strcmp(s, schemestrtab[i])==0)
-			return i;
-	return USunknown;
+	if('0' <= c && c <= '9')
+		return c-'0';
+	if('a' <= c && c <= 'f')
+		return c-'a'+10;
+	if('A' <= c && c <= 'F')
+		return c-'A'+10;
+	return 0;
 }
 
-/*
- * URI splitting regexp is from RFC2396, Appendix B: 
- *		^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
- *		 12            3  4          5       6  7        8 9
- *
- * Example: "http://www.ics.uci.edu/pub/ietf/uri/#Related"
- * $2 = scheme			"http"
- * $4 = authority		"www.ics.uci.edu"
- * $5 = path			"/pub/ietf/uri/"
- * $7 = query			<undefined>
- * $9 = fragment		"Related"
- */
-
-/*
- * RFC2396, Sec 3.1, contains:
- *
- * Scheme names consist of a sequence of characters beginning with a
- * lower case letter and followed by any combination of lower case
- * letters, digits, plus ("+"), period ("."), or hyphen ("-").  For
- * resiliency, programs interpreting URI should treat upper case letters
- * as equivalent to lower case in scheme names (e.g., allow "HTTP" as
- * well as "http").
- */
-
-/*
- * For server-based naming authorities (RFC2396 Sec 3.2.2):
- *    server        = [ [ userinfo "@" ] hostport ]
- *    userinfo      = *( unreserved | escaped |
- *                      ";" | ":" | "&" | "=" | "+" | "$" | "," )
- *    hostport      = host [ ":" port ]
- *    host          = hostname | IPv4address
- *    hostname      = *( domainlabel "." ) toplabel [ "." ]
- *    domainlabel   = alphanum | alphanum *( alphanum | "-" ) alphanum
- *    toplabel      = alpha | alpha *( alphanum | "-" ) alphanum
- *    IPv4address   = 1*digit "." 1*digit "." 1*digit "." 1*digit
- *    port          = *digit
- *
- *  The host is a domain name of a network host, or its IPv4 address as a
- *  set of four decimal digit groups separated by ".".  Literal IPv6
- *  addresses are not supported.
- *
- * Note that literal IPv6 address support is outlined in RFC2732:
- *    host          = hostname | IPv4address | IPv6reference
- *    ipv6reference = "[" IPv6address "]"		(RFC2373)
- *
- * Since hostnames and numbers will have to be resolved by the OS anyway,
- * we don't have to parse them too pedantically (counting '.'s, checking 
- * for well-formed literal IP addresses, etc.).
- *
- * In FTP/file paths, we reject most ";param"s and querys.  In HTTP paths,
- * we just pass them through.
- *
- * Instead of letting a "path" be 0-or-more characters as RFC2396 suggests, 
- * we'll say it's 1-or-more characters, 0-or-1 times.  This way, an absent
- * path yields a nil substring match, instead of an empty one.
- *
- * We're more restrictive than RFC2396 indicates with "userinfo" strings,
- * insisting they have the form "[user[:password]]".  This may need to
- * change at some point, however.
- */
-
-/* RE character-class components -- these go in brackets */
-#define PUNCT			"\\-_.!~*'()"
-#define ALNUM		"a-zA-Z0-9"
-#define HEX			"0-9a-fA-F"
-#define UNRES			ALNUM PUNCT
-
-/* RE components; _N => has N parenthesized subexpressions when expanded */
-#define USERINFO_2		"([" UNRES ";:&=+$,]|(%[" HEX "][" HEX "]))"
-
-typedef struct Retab Retab;
-struct Retab
+static char*
+unescape(char *s, char *spec)
 {
-	char	*str;
-	Reprog	*prog;
-	int		size;
-	int		ind[5];
-};
+	char *r, *w;
+	uchar x;
 
-enum
-{
-	REsplit = 0,
-	REscheme,
-	REauthority,
-	REhost,
-	REuserinfo,
-	REftppath,
-
-	MaxResub=	20,
-};
-
-Retab retab[] =	/* view in constant width Font */
-{
-[REsplit]
-	"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]+)?(\\?([^#]*))?(#(.*))?$", nil, 0,
-	/* |-scheme-|      |-auth.-|  |path--|    |query|     |--|frag */
-	{  2,              4,         5,          7,          9},
-
-[REscheme]
-	"^[a-z][a-z0-9+-.]*$", nil, 0,
-	{ 0, },
-
-[REauthority]
-	"^(((" USERINFO_2 "*)@)?(((\\[[^\\]@]+\\])|([^:\\[@]+))(:([0-9]*))?)?)?$", nil, 0,
-	/* |----user info-----|  |--------host----------------|  |-port-| */
-	{  3,                    7,                              11, },
-
-[REhost]
-	"^(([a-zA-Z0-9\\-.]+)|(\\[([a-fA-F0-9.:]+)\\]))$", nil, 0,
-	/* |--regular host--|     |-IPv6 literal-| */
-	{  2,                     4, },
-
-[REuserinfo]
-	"^(([^:]*)(:([^:]*))?)$", nil, 0,
-	/* |user-|  |pass-| */
-	{  2,       4, },
-
-[REftppath]
-	"^(.+)(;[tT][yY][pP][eE]=([aAiIdD]))?$", nil, 0,
-	/*|--|-path              |ftptype-| */
-	{ 1,                     3, }, 
-};
-
-static int
-countleftparen(char *s)
-{
-	int n;
-
-	n = 0;
-	for(; *s; s++)
-		if(*s == '(')
-			n++;
-	return n;
-}
-
-void
-initurl(void)
-{
-	int i, j;
-
-	for(i=0; i<nelem(retab); i++){
-		retab[i].prog = regcomp(retab[i].str);
-		if(retab[i].prog == nil)
-			sysfatal("recomp(%s): %r", retab[i].str);
-		retab[i].size = countleftparen(retab[i].str)+1;
-		for(j=0; j<nelem(retab[i].ind); j++)
-			if(retab[i].ind[j] >= retab[i].size)
-				sysfatal("bad index in regexp table: retab[%d].ind[%d] = %d >= %d",
-					i, j, retab[i].ind[j], retab[i].size);
-		if(MaxResub < retab[i].size)
-			sysfatal("MaxResub too small: %d < %d", MaxResub, retab[i].size);
-	}
-}
-
-typedef struct SplitUrl SplitUrl;
-struct SplitUrl
-{
-	struct {
-		char *s;
-		char *e;
-	} url, scheme, authority, path, query, fragment;
-};
-
-/*
- * Implements the algorithm in RFC2396 sec 5.2 step 6.
- * Returns number of chars written, excluding NUL terminator.
- * dest is known to be >= strlen(base)+rel_len.
- */
-static void
-merge_relative_path(char *base, char *rel_st, int rel_len, char *dest)
-{
-	char *s, *p, *e, *pdest;
-
-	pdest = dest;
-
-	/* 6a: start with base, discard last segment */
-	if(base && base[0]){
-		/* Empty paths don't match in our scheme; 'base' should be nil */
-		assert(base[0] == '/');
-		e = strrchr(base, '/');
-		e++;
-		memmove(pdest, base, e-base);
-		pdest += e-base;
-	}else{
-		/* Artistic license on my part */
-		*pdest++ = '/';
-	}
-
-	/* 6b: append relative component */
-	if(rel_st){
-		memmove(pdest, rel_st, rel_len);
-		pdest += rel_len;
-	}
-
-	/* 6c: remove any occurrences of "./" as a complete segment */
-	s = dest;
-	*pdest = '\0';
-	while(e = strstr(s, "./")){
-		if((e == dest) || (*(e-1) == '/')){
- 			memmove(e, e+2, pdest+1-(e+2));	/* +1 for NUL */
-			pdest -= 2;
-		}else
-			s = e+1;
-	}
-
-	/* 6d: remove a trailing "." as a complete segment */
-	if(pdest>dest && *(pdest-1)=='.' && 
-	  (pdest==dest+1 || *(pdest-2)=='/'))
-		*--pdest = '\0';
-
-	/* 6e: remove occurences of "seg/../", where seg != "..", left->right */
-	s = dest+1;
-	while(e = strstr(s, "/../")){
-		p = e - 1;
-		while(p >= dest && *p != '/')
-			p--;
-		if(memcmp(p, "/../", 4) != 0){
-			memmove(p+1, e+4, pdest+1-(e+4));
-			pdest -= (e+4) - (p+1);
-		}else
-			s = e+1;
-	}
-
-	/* 6f: remove a trailing "seg/..", where seg isn't ".."  */
-	if(pdest-3 > dest && memcmp(pdest-3, "/..", 3)==0){
-		p = pdest-3 - 1;
-		while(p >= dest && *p != '/')
-			p--;
-		if(memcmp(p, "/../", 4) != 0){
-			pdest = p+1;
-			*pdest = '\0';
+	if(s == nil)
+		return s;
+	for(r=w=s; x = *r; r++){
+		if(x == '%' && isxdigit(r[1]) && isxdigit(r[2])){
+			x = (dhex(r[1])<<4)|dhex(r[2]);
+			if(x == 0 || (x > 0x1F && x < 0x7F && strchr(spec, x))){
+				*w++ = '%';
+				*w++ = toupper(r[1]);
+				*w++ = toupper(r[2]);
+			}
+			else
+				*w++ = x;
+			r += 2;
+			continue;
 		}
+		if(x == '+')
+			x = ' ';
+		*w++ = x;
 	}
-
-	/* 6g: leading ".." segments are errors -- we'll just blat them out. */
-	if(RemoveExtraRelDotDots){
-		p = dest;
-		if (p[0] == '/')
-			p++;
-		s = p;
-		while(s[0]=='.' && s[1]=='.' && (s[2]==0 || s[2]=='/'))
-			s += 3;
-		if(s > p){
-			memmove(p, s, pdest+1-s);
-			pdest -= s-p;
-		}
-	}
-	USED(pdest);
-
-	if(urldebug)
-		fprint(2, "merge_relative_path: '%s' + '%.*s' -> '%s'\n", base, rel_len, 
-			rel_st, dest);
+	*w = 0;
+	return s;
 }
 
-/*
- * See RFC2396 sec 5.2 for info on resolving relative URIs to absolute form.
- *
- * If successful, this just ends up freeing and replacing "u->url".
- */
-static int
-resolve_relative(SplitUrl *su, Url *base, Url *u)
+int
+Efmt(Fmt *f)
 {
-	char *url, *path;
-	char *purl, *ppath;
-	int currentdoc, ulen, plen;
+	char *s, *spec;
+	Str2 s2;
 
-	if(base == nil){
-		werrstr("relative URI given without base");
-		return -1;
-	}
-	if(base->scheme == nil){
-		werrstr("relative URI given with no scheme");
-		return -1;
-	}
-	if(base->ischeme == USunknown){
-		werrstr("relative URI given with unknown scheme");
-		return -1;
-	}
-	if(base->ischeme == UScurrent){
-		werrstr("relative URI given with incomplete base");
-		return -1;
-	}
-	assert(su->scheme.s == nil);
-
-	/* Sec 5.2 step 2 */
-	currentdoc = 0;
-	if(su->path.s==nil && su->scheme.s==nil && su->authority.s==nil && su->query.s==nil){
-		/* Reference is to current document */
-		if(urldebug)
-			fprint(2, "url %s is relative to current document\n", u->url);
-		u->ischeme = UScurrent;
-		if(!ExpandCurrentDocUrls)
-			return 0;
-		currentdoc = 1;
-	}
-	
-	/* Over-estimate the maximum lengths, for allocation purposes */
-	/* (constants are for separators) */
-	plen = 1;
-	if(base->path)
-		plen += strlen(base->path);
-	if(su->path.s)
-		plen += 1 + (su->path.e - su->path.s);
-
-	ulen = 0;
-	ulen += strlen(base->scheme) + 1;
-	if(su->authority.s)
-		ulen += 2 + (su->authority.e - su->authority.s);
-	else
-		ulen += 2 + ((base->authority) ? strlen(base->authority) : 0);
-	ulen += plen;
-	if(su->query.s)
-		ulen += 1 + (su->query.e - su->query.s);
-	else if(currentdoc && base->query)
-		ulen += 1 + strlen(base->query);
-	if(su->fragment.s)
-		ulen += 1 + (su->fragment.e - su->fragment.s);
-	else if(currentdoc && base->fragment)
-		ulen += 1 + strlen(base->fragment);
-	url = emalloc(ulen+1);
-	path = emalloc(plen+1);
-
-	url[0] = '\0';
-	purl = url;
-	path[0] = '\0';
-	ppath = path;
-
-	if(su->authority.s || (su->path.s && (su->path.s[0] == '/'))){
-		/* Is a "network-path" or "absolute-path"; don't merge with base path */
-		/* Sec 5.2 steps 4,5 */
-		if(su->path.s){
-			memmove(ppath, su->path.s, su->path.e - su->path.s);
-			ppath += su->path.e - su->path.s;
-			*ppath = '\0';
+	s2 = va_arg(f->args, Str2);
+	s = s2.s1;
+	spec = s2.s2;
+	for(; *s; s++)
+		if(*s == '%' && isxdigit(s[1]) && isxdigit(s[2])){
+			fmtprint(f, "%%%c%c", toupper(s[1]), toupper(s[2]));
+			s += 2;
 		}
-	}else if(currentdoc){
-		/* Is a current-doc reference; just copy the path from the base URL */
-		if(base->path){
-			strcpy(ppath, base->path);
-			ppath += strlen(ppath);
-		}
-		USED(ppath);
-	}else{
-		/* Is a relative-path reference; we have to merge it */
-		/* Sec 5.2 step 6 */
-		merge_relative_path(base->path,
-			su->path.s, su->path.e - su->path.s, ppath);
-	}
-
-	/* Build new URL from pieces, inheriting from base where needed */
-	strcpy(purl, base->scheme);
-	purl += strlen(purl);
-	*purl++ = ':';
-	if(su->authority.s){
-		strcpy(purl, "//");
-		purl += strlen(purl);
-		memmove(purl, su->authority.s, su->authority.e - su->authority.s);
-		purl += su->authority.e - su->authority.s;
-	}else if(base->authority){
-		strcpy(purl, "//");
-		purl += strlen(purl);
-		strcpy(purl, base->authority);
-		purl += strlen(purl);
-	}
-	assert((path[0] == '\0') || (path[0] == '/'));
-	strcpy(purl, path);
-	purl += strlen(purl);
-
-	/*
-	 * The query and fragment are not inherited from the base,
-	 * except in case of "current document" URLs, which inherit any query
-	 * and may inherit the fragment.
-	 */
-	if(su->query.s){
-		*purl++ = '?';
-		memmove(purl, su->query.s, su->query.e - su->query.s);
-		purl += su->query.e - su->query.s;
-	}else if(currentdoc && base->query){
-		*purl++ = '?';
-		strcpy(purl, base->query);
-		purl += strlen(purl);
-	}
-
-	if(su->fragment.s){
-		*purl++ = '#';
-		memmove(purl, su->fragment.s, su->fragment.e - su->fragment.s);
-		purl += su->fragment.e - su->fragment.s;
-	}else if(currentdoc && base->fragment){
-		*purl++ = '#';
-		strcpy(purl, base->fragment);
-		purl += strlen(purl);
-	}
-	USED(purl);
-
-	if(urldebug)
-		fprint(2, "resolve_relative: '%s' + '%s' -> '%s'\n", base->url, u->url, url);
-	free(u->url);
-	u->url = url;
-	free(path);
+		else if(isalnum(*s) || strchr(".-_~!$&'()*,;=", *s) || strchr(spec, *s))
+			fmtprint(f, "%c", *s);
+		else
+			fmtprint(f, "%%%.2X", *s & 0xff);
 	return 0;
 }
 
 int
-regx(Reprog *prog, char *s, Resub *m, int nm)
+Ufmt(Fmt *f)
 {
-	int i;
+	char *s;
+	Url *u;
 
-	if(s == nil)
-		s = m[0].sp;	/* why is this necessary? */
-
-	i = regexec(prog, s, m, nm);
-/*
-	if(i >= 0)
-		for(j=0; j<nm; j++)
-			fprint(2, "match%d: %.*s\n", j, utfnlen(m[j].sp, m[j].ep-m[j].sp), m[j].sp);
-*/
-	return i;
-}
-
-static int
-ismatch(int i, char *s, char *desc)
-{
-	Resub m[1];
-
-	m[0].sp = m[0].ep = nil;
-	if(!regx(retab[i].prog, s, m, 1)){
-		werrstr("malformed %s: %q", desc, s);
-		return 0;
+	if((u = va_arg(f->args, Url*)) == nil)
+		return fmtprint(f, "nil");
+	if(u->scheme)
+		fmtprint(f, "%s:", u->scheme);
+	if(u->user || u->host)
+		fmtprint(f, "//");
+	if(u->user){
+		fmtprint(f, "%E", (Str2){u->user, ""});
+		if(u->pass)
+			fmtprint(f, ":%E", (Str2){u->pass, ""});
+		fmtprint(f, "@");
 	}
-	return 1;
-}
-
-static int
-spliturl(char *url, SplitUrl *su)
-{
-	Resub m[MaxResub];
-	Retab *t;
-
-	/*
-	 * Newlines are not valid in a URI, but regexp(2) treats them specially 
-	 * so it's best to make sure there are none before proceeding.
-	 */
-	if(strchr(url, '\n')){
-		werrstr("newline in URI");
-		return -1;
+	if(u->host){
+		fmtprint(f, strchr(u->host, ':') ? "[%s]" : "%s", u->host);
+		if(u->port)
+			fmtprint(f, ":%s", u->port);
 	}
-
-	m[0].sp = m[0].ep = nil;
-	t = &retab[REsplit];
-	if(!regx(t->prog, url, m, t->size)){
-		werrstr("malformed URI: %q", url);
-		return -1;
-	}
-
-	su->url.s = m[0].sp;
-	su->url.e = m[0].ep;
-	su->scheme.s = m[t->ind[0]].sp;
-	su->scheme.e = m[t->ind[0]].ep;
-	su->authority.s = m[t->ind[1]].sp;
-	su->authority.e = m[t->ind[1]].ep;
-	su->path.s = m[t->ind[2]].sp;
-	su->path.e = m[t->ind[2]].ep;
-	su->query.s = m[t->ind[3]].sp;
-	su->query.e = m[t->ind[3]].ep;
-	su->fragment.s = m[t->ind[4]].sp;
-	su->fragment.e = m[t->ind[4]].ep;
-
-	if(urldebug)
-		fprint(2, "split url %s into %.*q %.*q %.*q %.*q %.*q %.*q\n",
-			url,
-			su->url.s ? utfnlen(su->url.s, su->url.e-su->url.s) : 10, su->url.s ? su->url.s : "",
-			su->scheme.s ? utfnlen(su->scheme.s, su->scheme.e-su->scheme.s) : 10, su->scheme.s ? su->scheme.s : "",
-			su->authority.s ? utfnlen(su->authority.s, su->authority.e-su->authority.s) : 10, su->authority.s ? su->authority.s : "",
-			su->path.s ? utfnlen(su->path.s, su->path.e-su->path.s) : 10, su->path.s ? su->path.s : "",
-			su->query.s ? utfnlen(su->query.s, su->query.e-su->query.s) : 10, su->query.s ? su->query.s : "",
-			su->fragment.s ? utfnlen(su->fragment.s, su->fragment.e-su->fragment.s) : 10, su->fragment.s ? su->fragment.s : "");
-
+	if(s = Upath(u))
+		fmtprint(f, "%E", (Str2){s, "/:@"});
+	if(u->query)
+		fmtprint(f, "?%E", (Str2){u->query, "/:@"});
+	if(u->fragment)
+		fmtprint(f, "#%E", (Str2){u->fragment, "/:@?"});
 	return 0;
 }
 
-static int
-parse_scheme(SplitUrl *su, Url *u)
+char*
+Upath(Url *u)
 {
-	if(su->scheme.s == nil){
-		werrstr("missing scheme");
-		return -1;
+	if(u){
+		if(u->path)
+			return u->path;
+		if(u->user || u->host)
+			return "/";
 	}
-	u->scheme = estredup(su->scheme.s, su->scheme.e);
-	strlower(u->scheme);
-
-	if(!ismatch(REscheme, u->scheme, "scheme"))
-		return -1;
-
-	u->ischeme = ischeme(u->scheme);
-	if(urldebug)
-		fprint(2, "parse_scheme %s => %d\n", u->scheme, u->ischeme);
-	return 0;
+	return nil;
 }
 
-static int
-parse_unknown_part(SplitUrl *su, Url *u)
+static char*
+remdot(char *s)
 {
-	char *s, *e;
+	char *b, *d, *p;
+	int dir, n;
 
-	assert(u->ischeme == USunknown);
-	assert(su->scheme.e[0] == ':');
-
-	s = su->scheme.e+1;
-	if(su->fragment.s){
-		e = su->fragment.s-1;
-		assert(*e == '#');
-	}else
-		e = s+strlen(s);
-
-	u->schemedata = estredup(s, e);
-	return 0;
-}
-
-static int
-parse_userinfo(char *s, char *e, Url *u)
-{
-	Resub m[MaxResub];
-	Retab *t;
-
-	m[0].sp = s;
-	m[0].ep = e;
-	t = &retab[REuserinfo];
-	if(!regx(t->prog, nil, m, t->size)){
-		werrstr("malformed userinfo: %.*q", utfnlen(s, e-s), s);
-		return -1;
+	dir = 1;
+	b = d = s;
+	while(*s == '/')
+		s++;
+	for(; s; s = p){
+		if(p = strchr(s, '/'))
+			while(*p == '/')
+				*p++ = 0;
+		if(*s == '.' && ((s[1] == 0) || (s[1] == '.' && s[2] == 0))){
+			if(s[1] == '.')
+				while(d > b)
+					if(*--d == '/')
+						break;
+			dir = 1;
+			continue;
+		} else
+			dir = (p != nil);
+		n = strlen(s);
+		memmove(d+1, s, n);
+		*d = '/';
+		d += n+1;
 	}
-	if(m[t->ind[0]].sp)
-		u->user = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
-	if(m[t->ind[1]].sp)
-		u->user = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
-	return 0;
+	if(dir)
+		*d++ = '/';
+	*d = 0;
+	return b;
 }
 
-static int
-parse_host(char *s, char *e, Url *u)
+static char*
+abspath(char *s, char *b)
 {
-	Resub m[MaxResub];
-	Retab *t;
+	char *x, *a;
 
-	m[0].sp = s;
-	m[0].ep = e;
-	t = &retab[REhost];
-	if(!regx(t->prog, nil, m, t->size)){
-		werrstr("malformed host: %.*q", utfnlen(s, e-s), s);
-		return -1;
+	if(b && *b){
+		if(s == nil || *s == 0)
+			return estrdup(b);
+		if(*s != '/' && (x = strrchr(b, '/'))){
+			a = emalloc((x - b) + strlen(s) + 4);
+			sprint(a, "/%.*s/%s", (int)(x - b), b, s);
+			return remdot(a);
+		}
 	}
-
-	assert(m[t->ind[0]].sp || m[t->ind[1]].sp);
-
-	if(m[t->ind[0]].sp)	/* regular */
-		u->host = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
-	else
-		u->host = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
-	return 0;
+	if(s && *s){
+		if(*s != '/')
+			return estrdup(s);
+		a = emalloc(strlen(s) + 4);
+		sprint(a, "/%s", s);
+		return remdot(a);
+	}
+	return nil;
 }
 
-static int
-parse_authority(SplitUrl *su, Url *u)
+static void
+pstrdup(char **p)
 {
-	Resub m[MaxResub];
-	Retab *t;
-	char *host;
-	char *userinfo;
-
-	if(su->authority.s == nil)
-		return 0;
-
-	u->authority = estredup(su->authority.s, su->authority.e);
-	m[0].sp = m[0].ep = nil;
-	t = &retab[REauthority];
-	if(!regx(t->prog, u->authority, m, t->size)){
-		werrstr("malformed authority: %q", u->authority);
-		return -1;
+	if(p == nil || *p == nil)
+		return;
+	if(**p == 0){
+		*p = nil;
+		return;
 	}
-
-	if(m[t->ind[0]].sp)
-		if(parse_userinfo(m[t->ind[0]].sp, m[t->ind[0]].ep, u) < 0)
-			return -1;
-	if(m[t->ind[1]].sp)
-		if(parse_host(m[t->ind[1]].sp, m[t->ind[1]].ep, u) < 0)
-			return -1;
-	if(m[t->ind[2]].sp)
-		u->port = estredup(m[t->ind[2]].sp, m[t->ind[2]].ep);
-
-
-	if(urldebug > 0){
-		userinfo = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep); 
-		host = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
-		fprint(2, "port: %q, authority %q\n", u->port, u->authority);
-		fprint(2, "host %q, userinfo %q\n", host, userinfo);
-		free(host);
-		free(userinfo);
-	}
-	return 0;
+	*p = estrdup(*p);
 }
 
-static int
-parse_abspath(SplitUrl *su, Url *u)
+static char*
+mklowcase(char *s)
 {
-	char *s;
+	char *p;
 
-	if(su->path.s == nil)
-		return 0;
-	s = estredup(su->path.s, su->path.e);
-	u->path = unescapeurl(s, "/");
-	free(s);
-	return 0;
+	if(s == nil)
+		return s;
+	for(p = s; *p; p++)
+		*p = tolower(*p);
+	return s;
 }
 
-static int
-parse_query(SplitUrl *su, Url *u)
+Url*
+url(char *s, Url *b)
 {
-	char *s;
+	char *t, *p, *x, *y;
+	Url *u;
 
-	if(su->query.s == nil)
-		return 0;
-	s = estredup(su->query.s, su->query.e);
-	u->query = unescapeurl(s, "&;=/");
-	free(s);
-	return 0;
-}
-
-static int
-parse_fragment(SplitUrl *su, Url *u)
-{
-	char *s;
-
-	if(su->fragment.s == nil)
-		return 0;
-	s = estredup(su->fragment.s, su->fragment.e);
-	u->fragment = unescapeurl(s, "");
-	free(s);
-	return 0;
-}
-
-static int
-postparse_http(Url *u)
-{
-	char *p, *q;
-
-	u->open = httpopen;
-	u->read = httpread;
-	u->close = httpclose;
-
-	if(u->authority==nil){
-		werrstr("missing authority (hostname, port, etc.)");
-		return -1;
+	if(s == nil)
+		s = "";
+	t = nil;
+	s = p = estrdup(s);
+	u = emalloc(sizeof(*u));
+	for(; *p; p++){
+		if(*p == ':'){
+			if(p == s)
+				break;
+			*p++ = 0;
+			u->scheme = s;
+			b = nil;
+			goto Abs;
+		}
+		if(!isalpha(*p))
+			if((p == s) || ((!isdigit(*p) && strchr("+-.", *p) == nil)))
+				break;
 	}
-	if(u->host == nil){
-		werrstr("missing host specification");
-		return -1;
+	p = s;
+	if(b){
+		switch(*p){
+		case 0:
+			memmove(u, b, sizeof(*u));
+			goto Out;
+		case '#':
+			memmove(u, b, sizeof(*u));
+			u->fragment = p+1;
+			goto Out;
+		case '?':
+			memmove(u, b, sizeof(*u));
+			u->fragment = u->query = nil;
+			break;
+		case '/':
+			if(p[1] == '/'){
+				u->scheme = b->scheme;
+				b = nil;
+				break;
+			}
+		default:
+			memmove(u, b, sizeof(*u));
+			u->fragment = u->query = u->path = nil;
+			break;
+		}
 	}
-
-	if(u->path == nil){
-		u->http.page_spec = estrdup("/");
-		return 0;
+Abs:
+	if(x = strchr(p, '#')){
+		*x = 0;
+		u->fragment = x+1;
 	}
-	p = escapeurl(u->path, "/");
-	if(u->query){
-		q = escapeurl(u->query, "&;=/");
-		u->http.page_spec = emalloc(strlen(p)+1+strlen(q)+1);
-		strcpy(u->http.page_spec, p);
-		strcat(u->http.page_spec, "?");
-		strcat(u->http.page_spec, q);
-		free(q);
-		free(p);
-	}else
-		u->http.page_spec = p;
-	return 0;
-}
-
-static int
-postparse_ftp(Url *u)
-{
-	Resub m[MaxResub];
-	Retab *t;
-
-	if(u->authority==nil){
-		werrstr("missing authority (hostname, port, etc.)");
-		return -1;
+	if(x = strchr(p, '?')){
+		*x = 0;
+		u->query = x+1;
 	}
-	if(u->query){
-		werrstr("unexpected \"?query\" in ftp path");
-		return -1;
-	}
-	if(u->host == nil){
-		werrstr("missing host specification");
-		return -1;
-	}
-
-	if(u->path == nil){
-		u->ftp.path_spec = estrdup("/");
-		return 0;
-	}
-
-	m[0].sp = m[0].ep = nil;
-	t = &retab[REftppath];
-	if(!regx(t->prog, u->path, m, t->size)){
-		werrstr("malformed ftp path: %q", u->path);
-		return -1;
-	}
-
-	if(m[t->ind[0]].sp){
-		u->ftp.path_spec = estredup(m[t->ind[0]].sp, m[t->ind[0]].ep);
-		if(strchr(u->ftp.path_spec, ';')){
-			werrstr("unexpected \";param\" in ftp path");
-			return -1;
+	if(p[0] == '/' && p[1] == '/'){
+		p += 2;
+		if(x = strchr(p, '/')){
+			u->path = t = abspath(x, Upath(b));
+			*x = 0;
 		}
-	}else
-		u->ftp.path_spec = estrdup("/");
-
-	if(m[t->ind[1]].sp){
-		u->ftp.type = estredup(m[t->ind[1]].sp, m[t->ind[1]].ep);
-		strlower(u->ftp.type);
+		if(x = strchr(p, '@')){
+			*x = 0;
+			if(y = strchr(p, ':')){
+				*y = 0;
+				u->pass = y+1;
+			}
+			u->user = p;
+			p = x+1;
+		}
+		if((x = strrchr(p, ']')) == nil)
+			x = p;
+		if(x = strrchr(x, ':')){
+			*x = 0;
+			u->port = x+1;
+		}
+		if(x = strchr(p, '[')){
+			p = x+1;
+			if(y = strchr(p, ']'))
+				*y = 0;
+		}
+		u->host = p;
+	} else {
+		u->path = t = abspath(p, Upath(b));
 	}
-	return 0;
-}
+Out:
+	pstrdup(&u->scheme);
+	pstrdup(&u->user);
+	pstrdup(&u->pass);
+	pstrdup(&u->host);
+	pstrdup(&u->port);
+	pstrdup(&u->path);
+	pstrdup(&u->query);
+	pstrdup(&u->fragment);
+	free(s);
+	free(t);
 
-static int
-postparse_file(Url *u)
-{
-	if(u->user || u->passwd){
-		werrstr("user information not valid with file scheme");
-		return -1;
-	}
-	if(u->query){
-		werrstr("unexpected \"?query\" in file path");
-		return -1;
-	}
-	if(u->port){
-		werrstr("port not valid with file scheme");
-		return -1;
-	}
-	if(u->path == nil){
-		werrstr("missing path in file scheme");
-		return -1;
-	}
-	if(strchr(u->path, ';')){
-		werrstr("unexpected \";param\" in file path");
-		return -1;
-	}
+	unescape(u->user, "");
+	unescape(u->pass, "");
+	unescape(u->path, "/");
+	unescape(u->query, "&;=/?#");
+	unescape(u->fragment, "");
+	mklowcase(u->scheme);
+	mklowcase(u->host);
+	mklowcase(u->port);
 
-	/* "localhost" is equivalent to no host spec, we'll chose the latter */
-	if(u->host && cistrcmp(u->host, "localhost") == 0){
-		free(u->host);
-		u->host = nil;
-	}
-	return 0;
+	return u;
 }
 
-static int (*postparse[])(Url*) = {
-	nil,
-	postparse_http,
-	postparse_http,
-	postparse_ftp,
-	postparse_file,
-};
-
 Url*
-parseurl(char *url, Url *base)
+saneurl(Url *u)
 {
-	Url *u;
-	SplitUrl su;
-
-	if(urldebug)
-		fprint(2, "parseurl %s with base %s\n", url, base ? base->url : "<none>");
-
-	u = emalloc(sizeof(Url));
-	u->url = estrdup(url);
-	if(spliturl(u->url, &su) < 0){
-	Fail:
+	if(u == nil || u->scheme == nil || u->host == nil || Upath(u) == nil){
 		freeurl(u);
 		return nil;
 	}
-
-	/* RFC2396 sec 3.1 says relative URIs are distinguished by absent scheme */ 
-	if(su.scheme.s==nil){
-		if(urldebug)
-			fprint(2, "parseurl has nil scheme\n");
-		if(resolve_relative(&su, base, u) < 0 || spliturl(u->url, &su) < 0)
-			goto Fail;
-		if(u->ischeme == UScurrent){
-			/* 'u.url' refers to current document; set fragment and return */
-			if(parse_fragment(&su, u) < 0)
-				goto Fail;
-			goto Done;
+	if(u->port){
+		/* remove default ports */
+		switch(atoi(u->port)){
+		case 21:	if(!strcmp(u->scheme, "ftp"))	goto Defport; break;
+		case 70:	if(!strcmp(u->scheme, "gopher"))goto Defport; break;
+		case 80:	if(!strcmp(u->scheme, "http"))	goto Defport; break;
+		case 443:	if(!strcmp(u->scheme, "https"))	goto Defport; break;
+		default:	if(!strcmp(u->scheme, u->port))	goto Defport; break;
+		Defport:
+			free(u->port);
+			u->port = nil;
 		}
 	}
+	return u;
+}
 
-	if(parse_scheme(&su, u) < 0
-	|| parse_fragment(&su, u) < 0)
-		goto Fail;
+int
+matchurl(Url *u, Url *s)
+{
+	if(u){
+		char *a, *b;
 
-	if(u->ischeme == USunknown){
-		if(parse_unknown_part(&su, u) < 0)
-			goto Fail;
-		goto Done;
+		if(s == nil)
+			return 0;
+		if(u->scheme && (s->scheme == nil || strcmp(u->scheme, s->scheme)))
+			return 0;
+		if(u->user && (s->user == nil || strcmp(u->user, s->user)))
+			return 0;
+		if(u->host && (s->host == nil || strcmp(u->host, s->host)))
+			return 0;
+		if(u->port && (s->port == nil || strcmp(u->port, s->port)))
+			return 0;
+		if(a = Upath(u)){
+			b = Upath(s);
+			if(b == nil || strncmp(a, b, strlen(a)))
+				return 0;
+		}
 	}
-
-	if(parse_query(&su, u) < 0
-	|| parse_authority(&su, u) < 0
-	|| parse_abspath(&su, u) < 0)
-		goto Fail;
-
-	if(u->ischeme < nelem(postparse) && postparse[u->ischeme])
-		if((*postparse[u->ischeme])(u) < 0)
-			goto Fail;
-
-Done:
-	setmalloctag(u, getcallerpc(&url));
-	rewriteurl(u);
-	return u;
+	return 1;
 }
 
 void
@@ -873,162 +362,13 @@
 {
 	if(u == nil)
 		return;
-	free(u->url);
 	free(u->scheme);
-	free(u->schemedata);
-	free(u->authority);
 	free(u->user);
-	free(u->passwd);
+	free(u->pass);
 	free(u->host);
 	free(u->port);
 	free(u->path);
 	free(u->query);
 	free(u->fragment);
-	switch(u->ischeme){
-	case UShttp:
-	case UShttps:
-		free(u->http.page_spec);
-		break;
-	case USftp:
-		free(u->ftp.path_spec);
-		free(u->ftp.type);
-		break;
-	}
 	free(u);
 }
-
-void
-rewriteurl(Url *u)
-{
-	char *s;
-
-	if(u->scheme == nil)
-		return;
-	if(u->schemedata)
-		s = estrmanydup(u->scheme, ":", u->schemedata, nil);
-	else
-		s = estrmanydup(u->scheme, "://", 
-			u->user ? u->user : "",
-			u->passwd ? ":" : "", u->passwd ? u->passwd : "",
-			u->user ? "@" : "", u->host ? u->host : "", 
-			u->port ? ":" : "", u->port ? u->port : "",
-			u->path ? u->path : "",
-			u->query ? "?" : "", u->query ? u->query : "",
-			u->fragment ? "#" : "", u->fragment ? u->fragment : "",
-			nil);
-	free(u->url);
-	u->url = s;
-}
-
-int
-seturlquery(Url *u, char *query)
-{
-	if(query == nil){
-		free(u->query);
-		u->query = nil;
-		return 0;
-	}
-	free(u->query);
-	u->query = unescapeurl(query, "&;=/");
-	return 0;
-}
-
-static void
-dupp(char **p)
-{
-	if(*p)
-		*p = estrdup(*p);
-}
-
-Url*
-copyurl(Url *u)
-{
-	Url *v;
-
-	v = emalloc(sizeof(Url));
-	*v = *u;
-	dupp(&v->url);
-	dupp(&v->scheme);
-	dupp(&v->schemedata);
-	dupp(&v->authority);
-	dupp(&v->user);
-	dupp(&v->passwd);
-	dupp(&v->host);
-	dupp(&v->port);
-	dupp(&v->path);
-	dupp(&v->query);
-	dupp(&v->fragment);
-
-	switch(v->ischeme){
-	case UShttp:
-	case UShttps:
-		dupp(&v->http.page_spec);
-		break;
-	case USftp:
-		dupp(&v->ftp.path_spec);
-		dupp(&v->ftp.type);
-		break;
-	}
-	return v;
-}
-
-static int
-dhex(char c)
-{
-	if('0' <= c && c <= '9')
-		return c-'0';
-	if('a' <= c && c <= 'f')
-		return c-'a'+10;
-	if('A' <= c && c <= 'F')
-		return c-'A'+10;
-	return 0;
-}
-
-char*
-escapeurl(char *s, char *special)
-{
-	static char *hex = "0123456789abcdef";
-	char *t, *u;
-
-	t = u = emalloc(strlen(s)*3+1);
-	for(; *s; s++){
-		if((s[0] == '%' && isxdigit(s[1]) && isxdigit(s[2])) ||
-			(*s >= '0' && *s <= '9') || 
-			(*s >= 'a' && *s <= 'z') ||
-			(*s >= 'A' && *s <= 'Z') || 
-			strchr(".-_~", *s) || strchr(special, *s))
-			*u++ = *s;
-		else if(s[0] == ' ')
-			*u++ = '+';
-		else {
-			*u++ = '%';
-			*u++ = hex[(*s>>4)&0xF];
-			*u++ = hex[*s&0xF];
-		}
-	}
-	*u = '\0';
-	return t;
-}
-
-char*
-unescapeurl(char *s, char *special)
-{
-	char *r, *w;
-	Rune x;
-
-	s = estrdup(s);
-	for(r=w=s; x = *r; r++){
-		if(x=='%' && isxdigit(r[1]) && isxdigit(r[2])){
-			x = (dhex(r[1])<<4)|dhex(r[2]);
-			if(x == 0 || (x > 0x1F && x < 0x7F && strchr(special, x)))
-				x = *r;
-			else
-				r += 2;
-		} else if(x=='+')
-			x = ' ';
-		*w++ = x;
-	}
-	*w = '\0';
-	return s;
-}
-
--- a/sys/src/cmd/webfs/util.c
+++ /dev/null
@@ -1,86 +1,0 @@
-#include <u.h>
-#include <libc.h>
-#include <bio.h>
-#include <ndb.h>
-#include <fcall.h>
-#include <thread.h>
-#include <9p.h>
-#include <ctype.h>
-#include "dat.h"
-#include "fns.h"
-
-void*
-erealloc(void *a, uint n)
-{
-	a = realloc(a, n);
-	if(a == nil)
-		sysfatal("realloc %d: out of memory", n);
-	setrealloctag(a, getcallerpc(&a));
-	return a;
-}
-
-void*
-emalloc(uint n)
-{
-	void *a;
-
-	a = mallocz(n, 1);
-	if(a == nil)
-		sysfatal("malloc %d: out of memory", n);
-	setmalloctag(a, getcallerpc(&n));
-	return a;
-}
-
-char*
-estrdup(char *s)
-{
-	s = strdup(s);
-	if(s == nil)
-		sysfatal("strdup: out of memory");
-	setmalloctag(s, getcallerpc(&s));
-	return s;
-}
-
-char*
-estredup(char *s, char *e)
-{
-	char *t;
-
-	t = emalloc(e-s+1);
-	memmove(t, s, e-s);
-	t[e-s] = '\0';
-	setmalloctag(t, getcallerpc(&s));
-	return t;
-}
-
-char*
-estrmanydup(char *s, ...)
-{
-	char *p, *t;
-	int len;
-	va_list arg;
-
-	len = strlen(s);
-	va_start(arg, s);
-	while((p = va_arg(arg, char*)) != nil)
-		len += strlen(p);
-	len++;
-
-	t = emalloc(len);
-	strcpy(t, s);
-	va_start(arg, s);
-	while((p = va_arg(arg, char*)) != nil)
-		strcat(t, p);
-	return t;
-}
-
-char*
-strlower(char *s)
-{
-	char *t;
-
-	for(t=s; *t; t++)
-		if('A' <= *t && *t <= 'Z')
-			*t += 'a'-'A';
-	return s;
-}
--- a/sys/src/cmd/webfs/webget.c
+++ /dev/null
@@ -1,87 +1,0 @@
-/*
- * Sample client.
- */
-#include <u.h>
-#include <libc.h>
-
-void
-xfer(int from, int to)
-{
-	char buf[12*1024];
-	int n;
-
-	while((n = read(from, buf, sizeof buf)) > 0)
-		if(write(to, buf, n) < 0)
-			sysfatal("write failed: %r");
-	if(n < 0)
-		sysfatal("read failed: %r");
-}
-
-void
-usage(void)
-{
-	fprint(2, "usage: webget [-b baseurl] [-m mtpt] [-p postbody] url\n");
-	exits("usage");
-}
-
-void
-main(int argc, char **argv)
-{
-	int conn, ctlfd, fd, n;
-	char buf[128], *base, *mtpt, *post, *url;
-
-	mtpt = "/mnt/web";
-	post = nil;
-	base = nil;
-	ARGBEGIN{
-	default:
-		usage();
-	case 'b':
-		base = EARGF(usage());
-		break;
-	case 'm':
-		mtpt = EARGF(usage());
-		break;
-	case 'p':
-		post = EARGF(usage());
-		break;
-	}ARGEND;
-
-	if (argc != 1) 
-		usage();
-
-	url = argv[0];
- 
-	snprint(buf, sizeof buf, "%s/clone", mtpt);
-	if((ctlfd = open(buf, ORDWR)) < 0)
-		sysfatal("couldn't open %s: %r", buf);
-	if((n = read(ctlfd, buf, sizeof buf-1)) < 0)
-		sysfatal("reading clone: %r");
-	if(n == 0)
-		sysfatal("short read on clone");
-	buf[n] = '\0';
-	conn = atoi(buf);
-
-	if(base)
-		if(fprint(ctlfd, "baseurl %s", base) < 0)
-			sysfatal("baseurl ctl write: %r");
-
-	if(fprint(ctlfd, "url %s", url) <= 0)
-		sysfatal("get ctl write: %r");
-
-	if(post){
-		snprint(buf, sizeof buf, "%s/%d/postbody", mtpt, conn);
-		if((fd = open(buf, OWRITE)) < 0)
-			sysfatal("open %s: %r", buf);
-		if(write(fd, post, strlen(post)) < 0)
-			sysfatal("post write failed: %r");
-		close(fd);
-	}
-
-	snprint(buf, sizeof buf, "%s/%d/body", mtpt, conn);
-	if((fd = open(buf, OREAD)) < 0)
-		sysfatal("open %s: %r", buf);
-
-	xfer(fd, 1);
-	exits(nil);
-}
--- a/sys/src/cmd/webfsget.c
+++ /dev/null
@@ -1,85 +1,0 @@
-/* Example of how to use webfs */
-#include <u.h>
-#include <libc.h>
-
-void
-xfer(int from, int to)
-{
-	char buf[12*1024];
-	int n;
-
-	while((n = read(from, buf, sizeof buf)) > 0)
-		if(write(to, buf, n) < 0)
-			sysfatal("write failed: %r");
-	if(n < 0)
-		sysfatal("read failed: %r");
-}
-
-void
-usage(void)
-{
-	fprint(2, "usage: webfsget [-b baseurl] [-m mtpt] [-p postbody] url\n");
-	exits("usage");
-}
-
-void
-main(int argc, char **argv)
-{
-	int conn, ctlfd, fd, n;
-	char buf[128], *base, *mtpt, *post, *url;
-
-	mtpt = "/mnt/web";
-	post = nil;
-	base = nil;
-	ARGBEGIN{
-	default:
-		usage();
-	case 'b':
-		base = EARGF(usage());
-		break;
-	case 'm':
-		mtpt = EARGF(usage());
-		break;
-	case 'p':
-		post = EARGF(usage());
-		break;
-	}ARGEND;
-
-	if (argc != 1) 
-		usage();
-
-	url = argv[0];
- 
-	snprint(buf, sizeof buf, "%s/clone", mtpt);
-	if((ctlfd = open(buf, ORDWR)) < 0)
-		sysfatal("couldn't open %s: %r", buf);
-	if((n = read(ctlfd, buf, sizeof buf-1)) < 0)
-		sysfatal("reading clone: %r");
-	if(n == 0)
-		sysfatal("short read on clone");
-	buf[n] = '\0';
-	conn = atoi(buf);
-
-	if(base)
-		if(fprint(ctlfd, "baseurl %s", base) < 0)
-			sysfatal("baseurl ctl write: %r");
-
-	if(fprint(ctlfd, "url %s", url) <= 0)
-		sysfatal("get ctl write: %r");
-
-	if(post){
-		snprint(buf, sizeof buf, "%s/%d/postbody", mtpt, conn);
-		if((fd = open(buf, OWRITE)) < 0)
-			sysfatal("open %s: %r", buf);
-		if(write(fd, post, strlen(post)) < 0)
-			sysfatal("post write failed: %r");
-		close(fd);
-	}
-
-	snprint(buf, sizeof buf, "%s/%d/body", mtpt, conn);
-	if((fd = open(buf, OREAD)) < 0)
-		sysfatal("open %s: %r", buf);
-
-	xfer(fd, 1);
-	exits(nil);
-}
--