shithub: riscv

ref: 94916808dd9a53ac297ab537851791894b919828
dir: /sys/src/cmd/webfs/http.c/

View raw version
#include <u.h>
#include <libc.h>
#include <ctype.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>

#include "dat.h"
#include "fns.h"

#include <auth.h>
#include <mp.h>
#include <libsec.h>

typedef struct Hconn Hconn;
typedef struct Hpool Hpool;
typedef struct Hauth Hauth;

struct Hconn
{
	Hconn	*next;
	long	time;

	int	fd;
	int	ctl;
	int	keep;
	int	cancel;
	int	tunnel;
	int	len;
	char	addr[128];
	char	buf[8192+2];
};

struct Hpool
{
	QLock;

	Hconn	*head;
	int	active;

	int	limit;
	int	peer;
	int	idle;
};

struct Hauth
{
	Hauth	*next;
	Url	*url;
	char	*auth;
};

static Hpool hpool = {
	.limit	= 16,
	.peer	= 4,
	.idle	= 5,	/* seconds */
};

static QLock authlk;
static Hauth *hauth;

static void hclose(Hconn *h);

static int
tlstrace(char *fmt, ...)
{
	int r;
	va_list a;
	va_start(a, fmt);
	r = vfprint(2, fmt, a);
	va_end(a);
	return r;
}

static int
tlswrap(int fd, char *servername)
{
	TLSconn conn;

	memset(&conn, 0, sizeof(conn));
	if(debug)
		conn.trace = tlstrace;
	if(servername != nil)
		conn.serverName = smprint("%N", servername);
	if((fd = tlsClient(fd, &conn)) < 0){
		if(debug) fprint(2, "tlsClient: %r\n");
	}
	free(conn.cert);
	free(conn.sessionID);
	free(conn.serverName);
	return fd;
}

static Hconn*
hdial(Url *u, int cached)
{
	char addr[128];
	Hconn *h, *p;
	int fd, ctl;

	snprint(addr, sizeof(addr), "tcp!%s!%s", u->host, u->port ? u->port : u->scheme);

	qlock(&hpool);
	if(cached){
		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(proxy)
		snprint(addr, sizeof(addr), "tcp!%s!%s",
			proxy->host, proxy->port ? proxy->port : proxy->scheme);

	if((fd = dial(addr, 0, 0, &ctl)) < 0)
		return nil;

	if(proxy){
		if(strcmp(proxy->scheme, "https") == 0)
			fd = tlswrap(fd, proxy->host);
	} else {
		if(strcmp(u->scheme, "https") == 0)
			fd = tlswrap(fd, u->host);
	}
	if(fd < 0){
		close(ctl);
		return nil;
	}

	h = emalloc(sizeof(*h));
	h->next = nil;
	h->time = 0;
	h->cancel = 0;
	h->tunnel = 0;
	h->keep = cached;
	h->len = 0;
	h->fd = fd;
	h->ctl = ctl;

	if(proxy){
		h->tunnel = strcmp(u->scheme, "https") == 0;
		snprint(addr, sizeof(addr), "tcp!%s!%s", u->host, u->port ? u->port : u->scheme);
	}
	nstrcpy(h->addr, addr, sizeof(h->addr));

	return h;
}

static void
hcloseall(Hconn *x)
{
	Hconn *h;

	while(h = x){
		x = h->next;
		h->next = nil;
		h->keep = 0;
		hclose(h);
	}
}

static void
hclose(Hconn *h)
{
	Hconn *x, *t;
	int i, n;

	if(h == nil)
		return;

	qlock(&hpool);
	if(!h->tunnel && h->keep && h->fd >= 0){
		for(n = 0, i = 0, t = nil, x = hpool.head; x; x = x->next){
			if(strcmp(x->addr, h->addr) == 0)
				if(++n > hpool.peer)
					break;
			if(++i < hpool.limit)
				t = x;
		}
		if(x == nil){
			/* return connection to pool */
			h->time = time(0);
			h->next = hpool.head;
			hpool.head = h;

			/* cut off tail */
			if(t){
				x = t->next;
				t->next = nil;
			}

			i = h->next != nil;
			qunlock(&hpool);

			/* free the tail */
			hcloseall(x);

			/*
			 * if h is first one in pool, spawn proc to close
			 * idle connections.
			 */
			if(i == 0)
			if(rfork(RFMEM|RFPROC|RFNOWAIT) == 0){
				do {
					Hconn **xx;
					long now;

					sleep(1000);

					qlock(&hpool);
					now = time(0);

					x = nil;
					xx = &hpool.head;
					while(h = *xx){
						if((now - h->time) > hpool.idle){
							*xx = h->next;

							/* link to tail */
							h->next = x;
							x = h;
							continue;
						}
						xx = &h->next;
					}

					i = hpool.head != nil;
					qunlock(&hpool);

					/* free the tail */
					hcloseall(x);
				} while(i);
				exits(nil);
			}
			return;
		}
	}
	hpool.active--;
	qunlock(&hpool);

	if(debug)
		fprint(2, "hclose [%d] %s\n", hpool.active, h->addr);

	if(h->ctl >= 0)
		close(h->ctl);
	if(h->fd >= 0)
		close(h->fd);
	free(h);
}

static void
hhangup(Hconn *h)
{
	if(debug)
		fprint(2, "hangup pc=%p: %r\n", getcallerpc(&h));
	h->keep = 0;
	if(h->ctl >= 0)
		hangup(h->ctl);
}

static int
hread(Hconn *h, void *data, int len)
{
	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;
	}
	if((len = read(h->fd, data, len)) == 0)
		h->keep = 0;
	if(len < 0)
		hhangup(h);
	return len;
}

static int
hwrite(Hconn *h, void *data, int len)
{
	if(write(h->fd, data, len) != len){
		hhangup(h);
		return -1;
	}
	return len;
}

static int
hline(Hconn *h, char *data, int len, int cont)
{
	char *x, *y, *e;
	int n;

	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(*y != ' ' && *y != '\t')
							break;
					if(y >= e || *y == 0)
						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;
			}
		}
		n = sizeof(h->buf) - h->len;
		if(n <= 0)
			return 0;
		if(h->tunnel)
			n = 1;	/* do not read beyond header */
		if((n = read(h->fd, h->buf + h->len, n)) <= 0){
			hhangup(h);
			return -1;
		}
		h->len += n;
	}
}

static int
hauthgetkey(char *params)
{
	if(debug)
		fprint(2, "hauthgetkey %s\n", params);
	werrstr("needkey %s", params);
	return -1;
}

int
authenticate(Url *u, Url *ru, char *method, char *s)
{
	char oerr[ERRMAX], *user, *pass, *realm, *nonce, *opaque, *x;
	Hauth *a;
	Fmt fmt;
	int n;

	snprint(oerr, sizeof(oerr), "authentification failed");
	errstr(oerr, sizeof(oerr));

	user = u->user;
	pass = u->pass;
	realm = nonce = opaque = nil;
	if(!cistrncmp(s, "Basic ", 6)){
		UserPasswd *up;

		s += 6;
		if(x = cistrstr(s, "realm="))
			realm = unquote(x+6, &s);
		if(realm == nil)
			return -1;
		up = nil;
		if(user == nil || pass == nil){
			fmtstrinit(&fmt);
			fmtprint(&fmt, " realm=%q", realm);
			if(user)
				fmtprint(&fmt, " user=%q", user);
			if((s = fmtstrflush(&fmt)) == nil)
				return -1;
			up = auth_getuserpasswd(hauthgetkey,
				"proto=pass service=http server=%q%s", u->host, s);
			free(s);
			if(up == nil)
				return -1;
			user = up->user;
			pass = up->passwd;
		}
		fmtstrinit(&fmt);
		fmtprint(&fmt, "%s:%s", user ? user : "", pass ? pass : "");
		if(up){
			memset(up->user, 0, strlen(up->user));
			memset(up->passwd, 0, strlen(up->passwd));
			free(up);
		}
		if((s = fmtstrflush(&fmt)) == nil)
			return -1;
		n = strlen(s);
		fmtstrinit(&fmt);
		fmtprint(&fmt, "Basic %.*[", n, s);
		memset(s, 0, n);
		free(s);
		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;
		fmtstrinit(&fmt);
		fmtprint(&fmt, " realm=%q", realm);
		if(user)
			fmtprint(&fmt, " user=%q", user);
		if((s = fmtstrflush(&fmt)) == nil)
			return -1;
		nchal = snprint(chal, sizeof(chal), "%s %s %U", nonce, method, ru);
		n = auth_respond(chal, nchal, ouser, sizeof ouser, resp, sizeof resp, hauthgetkey,
			"proto=httpdigest role=client server=%q%s", u->host, s);
		memset(chal, 0, sizeof(chal));
		free(s);
		if(n < 0)
			return -1;
		fmtstrinit(&fmt);
		fmtprint(&fmt, "Digest ");
		fmtprint(&fmt, "username=\"%s\", ", ouser);
		fmtprint(&fmt, "realm=\"%s\", ", realm);
		fmtprint(&fmt, "host=\"%N\", ", 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((s = fmtstrflush(&fmt)) == nil){
		freeurl(u);
		return -1;
	}
	if(u == nil){
		free(s);
		return -1;
	}

	a = emalloc(sizeof(*a));
	a->url = u;
	a->auth = s;
	qlock(&authlk);
	a->next = hauth;
	hauth = a;
	qunlock(&authlk);

	errstr(oerr, sizeof(oerr));
	return 0;
}

int
hauthenticate(Url *u, Url *ru, char *method, char *key, Key *hdr)
{
	for(hdr = getkey(hdr, key); hdr != nil; hdr = getkey(hdr->next, key))
		if(authenticate(u, ru, method, hdr->val) == 0)
			return 0;
	return -1;
}

void
flushauth(Url *u, char *t)
{
	Hauth *a, *p;

	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);
}

static void
catch(void *, char *msg)
{
	if(strstr("alarm", msg) != nil)
		noted(NCONT);
	else
		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, needlength, chunked, retry, nobody, badauth;
	char *s, *x, buf[8192+2], status[256], method[16], *host;
	vlong length, offset;
	Url ru, tu, *nu;
	Key *k, *rhdr;
	Hconn *h;
	Hauth *a;

	incref(qbody);
	if(qpost) incref(qpost);
	nstrcpy(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;
	}

	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;

	h = nil;
	cfd = -1;
	pid = 0;
	host = nil;
	needlength = 0;
	badauth = 0;
	for(try = 0; try < 12; try++){
		strcpy(status, "0 No status");
		if(u == nil || (strcmp(u->scheme, "http") && strcmp(u->scheme, "https"))){
			werrstr("bad url scheme");
			break;
		}

		if(debug)
			fprint(2, "http(%d): %s %U\n", try, method, u);

		/* 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);

		length = 0;
		chunked = 0;
		if(qpost){
			/* have to read it to temp file to figure out the length */
			if(fd >= 0 && needlength && lookkey(shdr, "Content-Length") == nil){
				seek(fd, 0, 2);
				while((n = buread(qpost, buf, sizeof(buf))) > 0)
					write(fd, buf, n);
				shdr = delkey(shdr, "Transfer-Encoding");
			}

			qlock(qpost);
			/* wait until buffer is full, most posts are small */
			while(!qpost->closed && qpost->size < qpost->limit && qpost->nwq == 0)
				rsleep(&qpost->rz);

			if(lookkey(shdr, "Content-Length"))
				chunked = 0;
			else if(x = lookkey(shdr, "Transfer-Encoding"))
				chunked = cistrstr(x, "chunked") != nil;
			else if(chunked = !qpost->closed)
				shdr = addkey(shdr, "Transfer-Encoding", "chunked");
			else if(qpost->closed){
				if(fd >= 0){
					length = seek(fd, 0, 2);
					if(length < 0)
						length = 0;
				}
				length += qpost->size;
				snprint(buf, sizeof(buf), "%lld", length);
				shdr = addkey(shdr, "Content-Length", buf);
			}
			qunlock(qpost);
		}

		/* http requires ascii encoding of host */
		free(host);
		host = smprint("%N", u->host);

		if(proxy && strcmp(u->scheme, "https") != 0){
			ru = *u;
			ru.host = host;
			ru.fragment = nil;
		} else {
			memset(&ru, 0, sizeof(ru));
			ru.path = Upath(u);
			ru.query = u->query;
		}
		n = snprint(buf, sizeof(buf), "%s %U HTTP/1.1\r\nHost: %]%s%s\r\n",
			method, &ru, host, u->port ? ":" : "", u->port ? u->port : "");
		if(n >= sizeof(buf)-64){
			werrstr("request too large");
			break;
		}
		if(h == nil){
			alarm(timeout);
			if((h = hdial(u, qpost==nil)) == nil)
				break;
		}
		if(h->tunnel){
			n = snprint(buf, sizeof(buf), "CONNECT %]:%s HTTP/1.1\r\nHost: %]:%s\r\n",
				host, u->port ? u->port : "443",
				host, u->port ? u->port : "443");
		}
		else 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 = host;
			tu.path = Upath(u);
			fprint(cfd, "%U", &tu);
			for(;;){
				if(n >= sizeof(buf)-2){
					if(debug)
						fprint(2, "-> %.*s", utfnlen(buf, 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;
			}
		}

		for(k = shdr; k; k = k->next){
			/* only send proxy headers when establishing tunnel */
			if(h->tunnel && cistrncmp(k->key, "Proxy-", 6) != 0)
				continue;
			if(n > 0){
				if(debug)
					fprint(2, "-> %.*s", utfnlen(buf, n), buf);
				if(hwrite(h, buf, n) != n)
					goto Badflush;
			}
			n = snprint(buf, sizeof(buf)-2, "%s: %s\r\n", k->key, k->val);
		}
		n += snprint(buf+n, sizeof(buf)-n, "\r\n");
		if(debug)
			fprint(2, "-> %.*s", utfnlen(buf, n), buf);
		if(hwrite(h, buf, n) != n){
		Badflush:
			alarm(0);
			goto Retry;
		}

		if(qpost && !h->tunnel){
			h->cancel = 0;
			if((pid = rfork(RFMEM|RFPROC)) <= 0){
				int ifd;

				if((ifd = fd) >= 0)
					seek(ifd, 0, 0);
				while(!h->cancel){
					alarm(0);
					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;
					}
					alarm(timeout);
					if(chunked){
						char tmp[32];
						if(hwrite(h, tmp, snprint(tmp, sizeof(tmp), "%x\r\n", n)) < 0)
							break;
						buf[n++] = '\r';
						buf[n++] = '\n';
					}
					if(hwrite(h, buf, n) != n)
						break;
				}
				if(chunked){
					alarm(timeout);
					hwrite(h, "0\r\n\r\n", 5);
				}else
					h->keep = 0;
				if(pid == 0)
					exits(nil);
			}
			/* no timeout when posting */
			alarm(0);
		} else
			alarm(timeout);

		Cont:
		rhdr = 0;
		retry = 0;
		chunked = 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;
				}
				if(x[0])
					nstrcpy(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;
		}

		nobody = !cistrcmp(method, "HEAD");
		if((i = atoi(status)) < 0)
			i = 0;
		Status:
		switch(i){
		default:
			if(i % 100){
				i -= (i % 100);
				goto Status;
			}
			goto Error;
		case 100:	/* Continue */
		case 101:	/* Switching Protocols */
		case 102:	/* Processing */
		case 103:	/* Early Hints */
			while(k = rhdr){
				rhdr = k->next;
				free(k);
			}
			strcpy(status, "0 No status");
			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 */
			goto Error;
		case 411:	/* Length Required */
			if(qpost){
				needlength = 1;
				h->cancel = 1;
				retry = 1;
				break;
			}
		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;
			}
			shdr = delkey(shdr, "Content-Length");
			shdr = delkey(shdr, "Content-Type");
			shdr = delkey(shdr, "Transfer-Encoding");
			if(cistrcmp(method, "HEAD"))
				nstrcpy(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(badauth++)
					goto Error;
			}
			if(hauthenticate(u, &ru, method, "WWW-Authenticate", rhdr) < 0){
			Autherror:
				h->cancel = 1;
				rerrstr(buf, sizeof(buf));
				buclose(qbody, buf);
				buclose(qpost, buf);
				break;
			}
		}
		if(0){
		case 407:	/* Proxy Auth */
			if(proxy == nil)
				goto Error;
			if(x = lookkey(shdr, "Proxy-Authorization")){
				flushauth(proxy, x);
				if(badauth++)
					goto Error;
			}
			if(hauthenticate(proxy, proxy, method, "Proxy-Authenticate", rhdr) < 0)
				goto Autherror;
		}
		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 */
			if(h->tunnel)
				break;
			qbody->url = u; u = nil;
			qbody->hdr = rhdr; rhdr = nil;
			if(nobody)
				buclose(qbody, 0);
			break;
		}

		while(k = rhdr){
			rhdr = k->next;
			free(k);
		}

		/*
		 * 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");

		/*
		 * when 2xx response is given for the CONNECT request
		 * then the proxy server has established the connection.
		 */
		if(h->tunnel && !retry && (i/100) == 2){
			if((h->fd = tlswrap(h->fd, host)) < 0)
				break;

			/* proceed to the original request */
			h->tunnel = 0;
			continue;
		}

		if(!chunked && length == NOLENGTH)
			h->keep = 0;

		/*
		 * 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(!retry)
			break;
		Retry:
		if(cfd >= 0)
			close(cfd);
		if(pid > 0){
			waitpid();
			pid = 0;
		}
		hclose(h);
		h = nil;
	}
	alarm(0);
	snprint(buf, sizeof(buf), "%s %r", status);
	buclose(qbody, buf);
	bufree(qbody);

	if(qpost){
		if(pid > 0)
			waitpid();
		buclose(qpost, buf);
		bufree(qpost);
	}
	if(fd >= 0)
		close(fd);

	hclose(h);
	freeurl(u);
	free(host);

	while(k = shdr){
		shdr = k->next;
		free(k);
	}
	exits(nil);
}