Commit Diff


commit - d48ffc6605b4a30e0e4356cf2d53383c2a223067
commit + 3634fa709eb3bd3cacb762306ef62ff9698441b3
blob - 7f7e25a75e6ba2fbb4307a284ca9e1a15d229cbf
blob + 72f199e5cae6f54b4d7d57830d9d28c41671e755
--- web/Makefile
+++ web/Makefile
@@ -2,7 +2,7 @@
 
 PROG =		amused-web
 
-SOURCES =	web.c http.c ../ev.c ../log.c ../playlist.c ../xmalloc.c
+SOURCES =	web.c bufio.c http.c ../ev.c ../log.c ../playlist.c ../xmalloc.c
 
 OBJS =		${SOURCES:.c=.o}
 
blob - 8b1d5a2d69d12e84ecfd77f74f402475834a9147
blob + c95d30f7bb7de7c19f3cf49cef7f94c5d900f638
--- web/http.c
+++ web/http.c
@@ -27,6 +27,7 @@
 #include <string.h>
 #include <unistd.h>
 
+#include "bufio.h"
 #include "http.h"
 #include "log.h"
 #include "xmalloc.h"
@@ -35,112 +36,88 @@
 #define nitems(x) (sizeof(x)/sizeof(x[0]))
 #endif
 
-static int
-writeall(struct reswriter *res, const char *buf, size_t buflen)
+int
+http_init(struct client *clt, int fd)
 {
-	ssize_t	 nw;
-	size_t	 off;
-
-	for (off = 0; off < buflen; off += nw)
-		if ((nw = write(res->fd, buf + off, buflen - off)) == 0 ||
-		    nw == -1) {
-			if (nw == 0)
-				log_warnx("Unexpected EOF");
-			else
-				log_warn("write");
-			res->err = 1;
-			return -1;
-		}
-
+	memset(clt, 0, sizeof(*clt));
+	if (bufio_init(&clt->bio) == -1)
+		return -1;
+	bufio_set_fd(&clt->bio, fd);
 	return 0;
 }
 
 int
-http_parse(struct request *req, int fd)
+http_parse(struct client *clt)
 {
-	ssize_t		 nr;
-	size_t		 avail, len;
-	int		 done = 0, first = 1;
-	char		*s, *t, *line, *endln;
+	struct buffer	*rbuf = &clt->bio.rbuf;
+	struct request	*req = &clt->req;
+	size_t		 len;
+	uint8_t		*endln;
+	char		*s, *t, *line;
 	const char	*errstr, *m;
 
-	memset(req, 0, sizeof(*req));
-
-	while (!done) {
-		if (req->len == sizeof(req->buf)) {
-			log_warnx("not enough space");
+	while (!clt->reqdone) {
+		endln = memmem(rbuf->buf, rbuf->len, "\r\n", 2);
+		if (endln == NULL) {
+			errno = EAGAIN;
 			return -1;
 		}
 
-		avail = sizeof(req->buf) - req->len;
-		nr = read(fd, req->buf + req->len, avail);
-		if (nr <= 0) {
-			if (errno == EAGAIN || errno == EINTR)
-				continue;
-			if (nr == 0)
-				log_warnx("Unexpected EOF");
-			else
-				log_warn("read");
-			return -1;
-		}
-		req->len += nr;
+		line = rbuf->buf;
+		if (endln == rbuf->buf)
+			clt->reqdone = 1;
 
-		while ((endln = memmem(req->buf, req->len, "\r\n", 2))) {
-			line = req->buf;
-			if (endln == req->buf)
-				done = 1;
+		len = endln - rbuf->buf + 2;
+		while (len > 0 && (line[len - 1] == '\r' ||
+		    line[len - 1] == '\n' || line[len - 1] == ' ' ||
+		    line[len - 1] == '\t'))
+			line[--len] = '\0';
 
-			len = endln - req->buf + 2;
-			while (len > 0 && (line[len - 1] == '\r' ||
-			    line[len - 1] == '\n' || line[len - 1] == ' ' ||
-			    line[len - 1] == '\t'))
-				line[--len] = '\0';
-
-			if (first) {
-				first = 0;
-				if (!strncmp("GET ", line, 4)) {
-					req->method = METHOD_GET;
-					s = line + 4;
-				} else if (!strncmp("POST ", line, 5)) {
-					req->method = METHOD_POST;
-					s = line + 5;
-				}
-
-				t = strchr(s, ' ');
-				if (t == NULL)
-					t = s;
-				if (*t != '\0')
-					*t++ = '\0';
-				req->path = xstrdup(s);
-				if (!strcmp("HTTP/1.0", t))
-					req->version = HTTP_1_0;
-				else if (!strcmp("HTTP/1.1", t))
-					req->version = HTTP_1_1;
-				else {
-					log_warnx("unknown http version: %s",
-					    t);
-					return -1;
-				}
+		/* first line */
+		if (clt->req.method == METHOD_UNKNOWN) {
+			if (!strncmp("GET ", line, 4)) {
+				req->method = METHOD_GET;
+				s = line + 4;
+			} else if (!strncmp("POST ", line, 5)) {
+				req->method = METHOD_POST;
+				s = line + 5;
+			} else {
+				errno = EINVAL;
+				return -1;
 			}
 
-			if (!strncasecmp(line, "Content-Length:", 15)) {
-				line += 15;
-				line += strspn(line, " \t");
-				req->clen = strtonum(line, 0, LONG_MAX,
-				    &errstr);
-				if (errstr != NULL) {
-					log_warnx("content-length is %s: %s",
-					    errstr, line);
-					return -1;
-				}
+			t = strchr(s, ' ');
+			if (t == NULL)
+				t = s;
+			if (*t != '\0')
+				*t++ = '\0';
+			clt->req.path = xstrdup(s);
+			if (!strcmp(t, "HTTP/1.0"))
+				clt->req.version = HTTP_1_0;
+			else if (!strcmp(t, "HTTP/1.1")) {
+				clt->req.version = HTTP_1_1;
+				clt->chunked = 1;
+			} else {
+				log_warnx("unknown http version %s", t);
+				errno = EINVAL;
+				return -1;
 			}
+		}
 
-			len = endln - req->buf + 2;
-			memmove(req->buf, req->buf + len, req->len - len);
-			req->len -= len;
-			if (done)
-				break;
+		if (!strncasecmp(line, "Content-Length:", 15)) {
+			line += 15;
+			line += strspn(line, " \t");
+			clt->req.clen = strtonum(line, 0, LONG_MAX,
+			    &errstr);
+			if (errstr) {
+				log_warnx("content-length is %s: %s",
+				    errstr, line);
+				errno = EINVAL;
+				return -1;
+			}
 		}
+
+		buf_drain(rbuf, endln - rbuf->buf + 2);
 	}
 
 	if (req->method == METHOD_GET)
@@ -155,61 +132,46 @@ http_parse(struct request *req, int fd)
 }
 
 int
-http_read(struct request *req, int fd)
+http_read(struct client *clt)
 {
+	struct request *req = &clt->req;
 	size_t	 left;
-	ssize_t	 nr;
+	size_t	 nr;
 
-	if (req->clen > sizeof(req->buf) - 1) {
+	if (req->clen > sizeof(clt->buf) - 1) {
 		log_warnx("POST has more data then what can be accepted");
 		return -1;
 	}
 
 	/* clients may have sent more data than advertised */
-	if (req->clen < req->len)
+	if (req->clen < clt->len)
 		left = 0;
 	else
-		left = req->clen - req->len;
+		left = req->clen - clt->len;
 
-	while (left > 0) {
-		nr = read(fd, req->buf + req->len, left);
-		if (nr <= 0) {
-			if (nr == -1 && errno == EAGAIN)
-				continue;
-			if (nr == 0)
-				log_warnx("Unexpected EOF");
-			else
-				log_warn("read");
+	if (left > 0) {
+		nr = bufio_drain(&clt->bio, clt->buf + clt->len, left);
+		clt->len += nr;
+		if (nr < left) {
+			errno = EAGAIN;
 			return -1;
 		}
-		req->len += nr;
-		left -= nr;
 	}
 
-	req->buf[req->len] = '\0';
-	while (req->len > 0 && (req->buf[req->len - 1] == '\r' ||
-	    (req->buf[req->len - 1] == '\n')))
-		req->buf[--req->len] = '\0';
+	clt->buf[clt->len] = '\0';
+	while (clt->len > 0 && (clt->buf[clt->len - 1] == '\r' ||
+	    (clt->buf[clt->len - 1] == '\n')))
+		clt->buf[--clt->len] = '\0';
+
 	return 0;
 }
 
-void
-http_response_init(struct reswriter *res, struct request *req, int fd)
-{
-	memset(res, 0, sizeof(*res));
-	res->fd = fd;
-	res->chunked = req->version == HTTP_1_1;
-}
-
 int
-http_reply(struct reswriter *res, int code, const char *reason,
-    const char *ctype)
+http_reply(struct client *clt, int code, const char *reason, const char *ctype)
 {
 	const char	*location = NULL;
 	int		 r;
 
-	res->len = 0;	/* discard any leftover from reading */
-
 	log_debug("> %d %s", code, reason);
 
 	if (code >= 300 && code < 400) {
@@ -217,7 +179,7 @@ http_reply(struct reswriter *res, int code, const char
 		ctype = "text/html;charset=UTF-8";
 	}
 
-	r = snprintf(res->buf, sizeof(res->buf), "HTTP/1.1 %d %s\r\n"
+	r = bufio_compose_fmt(&clt->bio, "HTTP/1.1 %d %s\r\n"
 	    "Connection: close\r\n"
 	    "Cache-Control: no-store\r\n"
 	    "%s%s%s"
@@ -231,114 +193,63 @@ http_reply(struct reswriter *res, int code, const char
 	    location == NULL ? "" : "Location: ",
 	    location == NULL ? "" : location,
 	    location == NULL ? "" : "\r\n",
-	    res->chunked ? "Transfer-Encoding: chunked\r\n" : "");
-	if (r < 0 || (size_t)r >= sizeof(res->buf))
+	    clt->chunked ? "Transfer-Encoding: chunked\r\n" : "");
+	if (r == -1) {
+		clt->err = 1;
 		return -1;
+	}
 
-	if (writeall(res, res->buf, r) == -1)
-		return -1;
-
 	if (location) {
-		if (http_writes(res, "<a href='") == -1 ||
-		    http_htmlescape(res, location) == -1 ||
-		    http_writes(res, "'>") == -1 ||
-		    http_htmlescape(res, reason) == -1 ||
-		    http_writes(res, "</a>") == -1)
+		if (http_writes(clt, "<a href='") == -1 ||
+		    http_htmlescape(clt, location) == -1 ||
+		    http_writes(clt, "'>") == -1 ||
+		    http_htmlescape(clt, reason) == -1 ||
+		    http_writes(clt, "</a>") == -1)
 			return -1;
 	}
 
+	bufio_set_chunked(&clt->bio, clt->chunked);
 	return 0;
 }
 
 int
-http_flush(struct reswriter *res)
+http_flush(struct client *clt)
 {
-	struct iovec	 iov[3];
-	char		 buf[64];
-	ssize_t		 nw;
-	size_t		 i, tot;
-	int		 r;
-
-	if (res->err)
+	if (clt->err)
 		return -1;
 
-	if (res->len == 0)
+	if (clt->len == 0)
 		return 0;
 
-	if (!res->chunked) {
-		if (writeall(res, res->buf, res->len) == -1)
-			return -1;
-		res->len = 0;
-		return 0;
-	}
-
-	r = snprintf(buf, sizeof(buf), "%zx\r\n", res->len);
-	if (r < 0 || (size_t)r >= sizeof(buf)) {
-		log_warn("snprintf failed");
-		res->err = 1;
+	if (bufio_compose(&clt->bio, clt->buf, clt->len) == -1) {
+		clt->err = 1;
 		return -1;
 	}
 
-	memset(iov, 0, sizeof(iov));
+	clt->len = 0;
 
-	iov[0].iov_base = buf;
-	iov[0].iov_len = r;
-
-	iov[1].iov_base = res->buf;
-	iov[1].iov_len = res->len;
-
-	iov[2].iov_base = "\r\n";
-	iov[2].iov_len = 2;
-
-	tot = iov[0].iov_len + iov[1].iov_len + iov[2].iov_len;
-	while (tot > 0) {
-		nw = writev(res->fd, iov, nitems(iov));
-		if (nw <= 0) {
-			if (nw == -1 && errno == EAGAIN)
-				continue;
-			if (nw == 0)
-				log_warnx("Unexpected EOF");
-			else
-				log_warn("writev");
-			res->err = 1;
-			return -1;
-		}
-
-		tot -= nw;
-		for (i = 0; i < nitems(iov); ++i) {
-			if (nw < iov[i].iov_len) {
-				iov[i].iov_base += nw;
-				iov[i].iov_len -= nw;
-				break;
-			}
-			nw -= iov[i].iov_len;
-			iov[i].iov_len = 0;
-		}
-	}
-
-	res->len = 0;
 	return 0;
 }
 
 int
-http_write(struct reswriter *res, const char *d, size_t len)
+http_write(struct client *clt, const char *d, size_t len)
 {
 	size_t		avail;
 
-	if (res->err)
+	if (clt->err)
 		return -1;
 
 	while (len > 0) {
-		avail = sizeof(res->buf) - res->len;
+		avail = sizeof(clt->buf) - clt->len;
 		if (avail > len)
 			avail = len;
 
-		memcpy(res->buf + res->len, d, avail);
-		res->len += avail;
+		memcpy(clt->buf + clt->len, d, avail);
+		clt->len += avail;
 		len -= avail;
 		d += avail;
-		if (res->len == sizeof(res->buf)) {
-			if (http_flush(res) == -1)
+		if (clt->len == sizeof(clt->buf)) {
+			if (http_flush(clt) == -1)
 				return -1;
 		}
 	}
@@ -347,13 +258,13 @@ http_write(struct reswriter *res, const char *d, size_
 }
 
 int
-http_writes(struct reswriter *res, const char *str)
+http_writes(struct client *clt, const char *str)
 {
-	return http_write(res, str, strlen(str));
+	return http_write(clt, str, strlen(str));
 }
 
 int
-http_fmt(struct reswriter *res, const char *fmt, ...)
+http_fmt(struct client *clt, const char *fmt, ...)
 {
 	va_list	 ap;
 	char	*str;
@@ -365,17 +276,17 @@ http_fmt(struct reswriter *res, const char *fmt, ...)
 
 	if (r == -1) {
 		log_warn("vasprintf");
-		res->err = 1;
+		clt->err = 1;
 		return -1;
 	}
 
-	r = http_write(res, str, r);
+	r = http_write(clt, str, r);
 	free(str);
 	return r;
 }
 
 int
-http_urlescape(struct reswriter *res, const char *str)
+http_urlescape(struct client *clt, const char *str)
 {
 	int	 r;
 	char	 tmp[4];
@@ -388,12 +299,12 @@ http_urlescape(struct reswriter *res, const char *str)
 			    (unsigned char)*str);
 			if (r < 0 || (size_t)r >= sizeof(tmp)) {
 				log_warn("snprintf failed");
-				res->err = 1;
+				clt->err = 1;
 				return -1;
 			}
-			if (http_write(res, tmp, r) == -1)
+			if (http_write(clt, tmp, r) == -1)
 				return -1;
-		} else if (http_write(res, str, 1) == -1)
+		} else if (http_write(clt, str, 1) == -1)
 			return -1;
 	}
 
@@ -401,29 +312,29 @@ http_urlescape(struct reswriter *res, const char *str)
 }
 
 int
-http_htmlescape(struct reswriter *res, const char *str)
+http_htmlescape(struct client *clt, const char *str)
 {
 	int r;
 
 	for (; *str; ++str) {
 		switch (*str) {
 		case '<':
-			r = http_writes(res, "&lt;");
+			r = http_writes(clt, "&lt;");
 			break;
 		case '>':
-			r = http_writes(res, "&gt;");
+			r = http_writes(clt, "&gt;");
 			break;
 		case '&':
-			r = http_writes(res, "&gt;");
+			r = http_writes(clt, "&gt;");
 			break;
 		case '"':
-			r = http_writes(res, "&quot;");
+			r = http_writes(clt, "&quot;");
 			break;
 		case '\'':
-			r = http_writes(res, "&apos;");
+			r = http_writes(clt, "&apos;");
 			break;
 		default:
-			r = http_write(res, str, 1);
+			r = http_write(clt, str, 1);
 			break;
 		}
 
@@ -435,17 +346,22 @@ http_htmlescape(struct reswriter *res, const char *str
 }
 
 int
-http_close(struct reswriter *res)
+http_close(struct client *clt)
 {
-	if (!res->chunked)
-		return 0;
-
-	return writeall(res, "0\r\n\r\n", 5);
+	if (clt->err)
+		return -1;
+	if (clt->len != 0 && http_flush(clt) == -1)
+		return -1;
+	if (bufio_compose(&clt->bio, NULL, 0) == -1)
+		clt->err = 1;
+	return (clt->err ? -1 : 0);
 }
 
 void
-http_free_request(struct request *req)
+http_free(struct client *clt)
 {
-	free(req->path);
-	free(req->ctype);
+	free(clt->req.path);
+	free(clt->req.ctype);
+	free(clt->req.body);
+	bufio_free(&clt->bio);
 }
blob - 51cbdb77bac5f32482d1db3184660b2a6e2cac12
blob + 78bdce3d680a99c406486891feb0985b42453f0c
--- web/http.h
+++ web/http.h
@@ -25,34 +25,41 @@ enum http_version {
 	HTTP_1_1,
 };
 
-struct request {
-	char	buf[BUFSIZ];
-	size_t	len;
+struct bufio;
 
+struct request {
 	char	*path;
 	int	 method;
 	int	 version;
 	char	*ctype;
+	char	*body;
 	size_t	 clen;
 };
 
-struct reswriter {
-	int	fd;
-	int	err;
-	int	chunked;
-	char	buf[BUFSIZ];
-	size_t	len;
+struct client;
+typedef void (*route_fn)(struct client *);
+
+struct client {
+	char		buf[1024];
+	size_t		len;
+	struct bufio	bio;
+	struct request	req;
+	int		err;
+	int		chunked;
+	int		reqdone;	/* done parsing the request */
+	int		done;		/* done handling the client */
+	route_fn	route;
 };
 
-int	http_parse(struct request *, int);
-int	http_read(struct request *, int);
-void	http_response_init(struct reswriter *, struct request *, int);
-int	http_reply(struct reswriter *, int, const char *, const char *);
-int	http_flush(struct reswriter *);
-int	http_write(struct reswriter *, const char *, size_t);
-int	http_writes(struct reswriter *, const char *);
-int	http_fmt(struct reswriter *, const char *, ...);
-int	http_urlescape(struct reswriter *, const char *);
-int	http_htmlescape(struct reswriter *, const char *);
-int	http_close(struct reswriter *);
-void	http_free_request(struct request *);
+int	http_init(struct client *, int);
+int	http_parse(struct client *);
+int	http_read(struct client *);
+int	http_reply(struct client *, int, const char *, const char *);
+int	http_flush(struct client *);
+int	http_write(struct client *, const char *, size_t);
+int	http_writes(struct client *, const char *);
+int	http_fmt(struct client *, const char *, ...);
+int	http_urlescape(struct client *, const char *);
+int	http_htmlescape(struct client *, const char *);
+int	http_close(struct client *);
+void	http_free(struct client *);
blob - 9b42ad5d1de05d252f9d2cac33ae314593e68159
blob + 824037445c78d40c98b663473a375c24371fb296
--- web/web.c
+++ web/web.c
@@ -36,6 +36,7 @@
 #include <unistd.h>
 
 #include "amused.h"
+#include "bufio.h"
 #include "ev.h"
 #include "http.h"
 #include "log.h"
@@ -249,15 +250,15 @@ unexpected_imsg(struct imsg *imsg, const char *expecte
 }
 
 static void
-route_notfound(struct reswriter *res, struct request *req)
+route_notfound(struct client *clt)
 {
-	if (http_reply(res, 404, "Not Found", "text/plain") == -1 ||
-	    http_writes(res, "Page not found\n") == -1)
+	if (http_reply(clt, 404, "Not Found", "text/plain") == -1 ||
+	    http_writes(clt, "Page not found\n") == -1)
 		return;
 }
 
 static void
-render_playlist(struct reswriter *res)
+render_playlist(struct client *clt)
 {
 	struct imsg		 imsg;
 	struct player_status	 ps;
@@ -268,10 +269,10 @@ render_playlist(struct reswriter *res)
 	imsg_compose(&ibuf, IMSG_CTL_SHOW, 0, 0, -1, NULL, 0);
 	imsg_flush(&ibuf);
 
-	http_writes(res, "<section class='playlist-wrapper'>");
-	http_writes(res, "<form action=jump method=post"
+	http_writes(clt, "<section class='playlist-wrapper'>");
+	http_writes(clt, "<form action=jump method=post"
 	    " enctype='"FORM_URLENCODED"'>");
-	http_writes(res, "<ul class=playlist>");
+	http_writes(clt, "<ul class=playlist>");
 
 	done = 0;
 	while (!done) {
@@ -309,26 +310,26 @@ render_playlist(struct reswriter *res)
 			if (!strncmp(p, prefix, prefixlen))
 				p += prefixlen;
 
-			http_fmt(res, "<li%s>",
+			http_fmt(clt, "<li%s>",
 			    current ? " id=current" : "");
-			http_writes(res,
+			http_writes(clt,
 			    "<button type=submit name=jump value=\"");
-			http_htmlescape(res, ps.path);
-			http_writes(res, "\">");
-			http_htmlescape(res, p);
-			http_writes(res, "</button></li>");
+			http_htmlescape(clt, ps.path);
+			http_writes(clt, "\">");
+			http_htmlescape(clt, p);
+			http_writes(clt, "</button></li>");
 
 			imsg_free(&imsg);
 		}
 	}
 
-	http_writes(res, "</ul>");
-	http_writes(res, "</form>");
-	http_writes(res, "</section>");
+	http_writes(clt, "</ul>");
+	http_writes(clt, "</form>");
+	http_writes(clt, "</section>");
 }
 
 static void
-render_controls(struct reswriter *res)
+render_controls(struct client *clt)
 {
 	struct imsg		 imsg;
 	struct player_status	 ps;
@@ -368,28 +369,28 @@ render_controls(struct reswriter *res)
 	else
 		p = ps.path;
 
-	if (http_writes(res, "<section class=controls>") == -1 ||
-	    http_writes(res, "<p><a href='#current'>") == -1 ||
-	    http_htmlescape(res, p) == -1 ||
-	    http_writes(res, "</a></p>") == -1 ||
-	    http_writes(res, "<form action=ctrls method=post"
+	if (http_writes(clt, "<section class=controls>") == -1 ||
+	    http_writes(clt, "<p><a href='#current'>") == -1 ||
+	    http_htmlescape(clt, p) == -1 ||
+	    http_writes(clt, "</a></p>") == -1 ||
+	    http_writes(clt, "<form action=ctrls method=post"
 		" enctype='"FORM_URLENCODED"'>") == -1 ||
-	    http_writes(res, "<button type=submit name=ctl value=prev>"
+	    http_writes(clt, "<button type=submit name=ctl value=prev>"
 		ICON_PREV"</button>") == -1 ||
-	    http_fmt(res, "<button type=submit name=ctl value=%s>"
+	    http_fmt(clt, "<button type=submit name=ctl value=%s>"
 		"%s</button>", playing ? "pause" : "play",
 		playing ? ICON_PAUSE : ICON_PLAY) == -1 ||
-	    http_writes(res, "<button type=submit name=ctl value=next>"
+	    http_writes(clt, "<button type=submit name=ctl value=next>"
 		ICON_NEXT"</button>") == -1 ||
-	    http_writes(res, "</form>") == -1 ||
-	    http_writes(res, "<form action=mode method=post"
-		" enctype='"FORM_URLENCODED"'>") == -1 ||
-	    http_fmt(res, "<button%s type=submit name=mode value=all>"
+	    http_writes(clt, "</form>") == -1 ||
+	    http_writes(clt, "<form action=mode method=post"
+		" enctype='"FORM_URLENCODED"'>") == -1 ||
+	    http_fmt(clt, "<button%s type=submit name=mode value=all>"
 		ICON_REPEAT_ALL"</button>", ac) == -1 ||
-	    http_fmt(res, "<button%s type=submit name=mode value=one>"
+	    http_fmt(clt, "<button%s type=submit name=mode value=one>"
 		ICON_REPEAT_ONE"</button>", oc) == -1 ||
-	    http_writes(res, "</form>") == -1 ||
-	    http_writes(res, "</section>") == -1)
+	    http_writes(clt, "</form>") == -1 ||
+	    http_writes(clt, "</section>") == -1)
 		return;
 
  done:
@@ -397,34 +398,34 @@ render_controls(struct reswriter *res)
 }
 
 static void
-route_home(struct reswriter *res, struct request *req)
+route_home(struct client *clt)
 {
-	if (http_reply(res, 200, "OK", "text/html;charset=UTF-8") == -1)
+	if (http_reply(clt, 200, "OK", "text/html;charset=UTF-8") == -1)
 		return;
 
-	if (http_write(res, head, strlen(head)) == -1)
+	if (http_write(clt, head, strlen(head)) == -1)
 		return;
 
-	if (http_writes(res, "<main>") == -1)
+	if (http_writes(clt, "<main>") == -1)
 		return;
 
-	if (http_writes(res, "<section class=searchbox>"
+	if (http_writes(clt, "<section class=searchbox>"
 	    "<input type=search name=filter aria-label='Filter playlist'"
 	    " placeholder='Filter playlist' id=search />"
 	    "</section>") == -1)
 		return;
 
-	render_controls(res);
-	render_playlist(res);
+	render_controls(clt);
+	render_playlist(clt);
 
-	if (http_writes(res, "</main>") == -1)
+	if (http_writes(clt, "</main>") == -1)
 		return;
 
-	http_write(res, foot, strlen(foot));
+	http_write(clt, foot, strlen(foot));
 }
 
 static void
-route_jump(struct reswriter *res, struct request *req)
+route_jump(struct client *clt)
 {
 	struct imsg		 imsg;
 	struct player_status	 ps;
@@ -433,10 +434,7 @@ route_jump(struct reswriter *res, struct request *req)
 	char			*form, *field;
 	int			 found = 0;
 
-	if (http_read(req, res->fd) == -1)
-		return;
-
-	form = req->buf;
+	form = clt->buf;
 	while ((field = strsep(&form, "&")) != NULL) {
 		if (url_decode(field) == -1)
 			goto badreq;
@@ -486,24 +484,21 @@ route_jump(struct reswriter *res, struct request *req)
 	if (!found)
 		goto badreq;
 
-	http_reply(res, 302, "See Other", "/");
+	http_reply(clt, 302, "See Other", "/");
 	return;
 
  badreq:
-	http_reply(res, 400, "Bad Request", "text/plain");
-	http_writes(res, "Bad Request.\n");
+	http_reply(clt, 400, "Bad Request", "text/plain");
+	http_writes(clt, "Bad Request.\n");
 }
 
 static void
-route_controls(struct reswriter *res, struct request *req)
+route_controls(struct client *clt)
 {
 	char		*form, *field;
 	int		 cmd, found = 0;
 
-	if (http_read(req, res->fd) == -1)
-		return;
-
-	form = req->buf;
+	form = clt->buf;
 	while ((field = strsep(&form, "&")) != NULL) {
 		if (url_decode(field) == -1)
 			goto badreq;
@@ -532,16 +527,16 @@ route_controls(struct reswriter *res, struct request *
 	if (!found)
 		goto badreq;
 
-	http_reply(res, 302, "See Other", "/");
+	http_reply(clt, 302, "See Other", "/");
 	return;
 
  badreq:
-	http_reply(res, 400, "Bad Request", "text/plain");
-	http_writes(res, "Bad Request.\n");
+	http_reply(clt, 400, "Bad Request", "text/plain");
+	http_writes(clt, "Bad Request.\n");
 }
 
 static void
-route_mode(struct reswriter *res, struct request *req)
+route_mode(struct client *clt)
 {
 	char			*form, *field;
 	int			 found = 0;
@@ -552,10 +547,7 @@ route_mode(struct reswriter *res, struct request *req)
 
 	pm.repeat_one = pm.repeat_all = pm.consume = MODE_UNDEF;
 
-	if (http_read(req, res->fd) == -1)
-		return;
-
-	form = req->buf;
+	form = clt->buf;
 	while ((field = strsep(&form, "&")) != NULL) {
 		if (url_decode(field) == -1)
 			goto badreq;
@@ -605,21 +597,21 @@ route_mode(struct reswriter *res, struct request *req)
 	if (!found)
 		goto badreq;
 
-	http_reply(res, 302, "See Other", "/");
+	http_reply(clt, 302, "See Other", "/");
 	return;
 
  badreq:
-	http_reply(res, 400, "Bad Request", "text/plain");
-	http_writes(res, "Bad Request.\n");
+	http_reply(clt, 400, "Bad Request", "text/plain");
+	http_writes(clt, "Bad Request.\n");
 }
 
 static void
-route_dispatch(struct reswriter *res, struct request *req)
+route_dispatch(struct client *clt)
 {
 	static const struct route {
-		int method;
-		const char *path;
-		void (*fn)(struct reswriter *, struct request *);
+		int		 method;
+		const char	*path;
+		route_fn	 route;
 	} routes[] = {
 		{ METHOD_GET,	"/",		&route_home },
 		{ METHOD_POST,	"/jump",	&route_jump },
@@ -629,12 +621,13 @@ route_dispatch(struct reswriter *res, struct request *
 		{ METHOD_GET,	"*",		&route_notfound },
 		{ METHOD_POST,	"*",		&route_notfound },
 	};
+	struct request *req = &clt->req;
 	size_t i;
 
 	if ((req->method != METHOD_GET && req->method != METHOD_POST) ||
 	    (req->ctype != NULL && strcmp(req->ctype, FORM_URLENCODED) != 0) ||
 	    req->path == NULL) {
-		http_reply(res, 400, "Bad Request", NULL);
+		http_reply(clt, 400, "Bad Request", NULL);
 		return;
 	}
 
@@ -642,32 +635,89 @@ route_dispatch(struct reswriter *res, struct request *
 		if (req->method != routes[i].method ||
 		    fnmatch(routes[i].path, req->path, 0) != 0)
 			continue;
-		routes[i].fn(res, req);
+		clt->done = 1; /* assume with one round is done */
+		clt->route = routes[i].route;
+		clt->route(clt);
+		if (clt->done)
+			http_close(clt);
 		return;
 	}
 }
 
 static void
+client_ev(int fd, int ev, void *d)
+{
+	struct client	*clt = d;
+
+	if (ev & (POLLIN|POLLHUP)) {
+		if (bufio_read(&clt->bio) == -1 && errno != EAGAIN) {
+			log_warn("bufio_read");
+			goto err;
+		}
+	}
+
+	if (ev & POLLOUT) {
+		if (bufio_write(&clt->bio) == -1 && errno != EAGAIN) {
+			log_warn("bufio_read");
+			goto err;
+		}
+	}
+
+	if (clt->route == NULL) {
+		if (http_parse(clt) == -1) {
+			if (errno == EAGAIN)
+				goto again;
+			log_warnx("HTTP parse request failed");
+			goto err;
+		}
+		if (clt->req.method == METHOD_POST &&
+		    http_read(clt) == -1) {
+			if (errno == EAGAIN)
+				goto again;
+			log_warnx("failed to read POST data");
+			goto err;
+		}
+		route_dispatch(clt);
+		goto again;
+	}
+
+	if (!clt->done)
+		clt->route(clt);
+
+ again:
+	ev = bufio_pollev(&clt->bio);
+	if (ev == POLLIN && clt->done) {
+		goto err; /* done with this client */
+	}
+
+	ev_add(fd, ev, client_ev, clt);
+	return;
+
+ err:
+	ev_del(fd);
+	http_free(clt);
+}
+
+static void
 web_accept(int psock, int ev, void *d)
 {
-	struct reswriter res;
-	struct request	 req;
+	struct client	*clt;
 	int		 sock;
 
 	if ((sock = accept(psock, NULL, NULL)) == -1) {
 		warn("accept");
 		return;
 	}
-	if (http_parse(&req, sock) == -1) {
+	clt = xcalloc(1, sizeof(*clt));
+	if ((clt = calloc(1, sizeof(*clt))) == NULL ||
+	    http_init(clt, sock) == -1) {
+		log_warn("failed to initialize client");
+		free(clt);
 		close(sock);
 		return;
 	}
-	http_response_init(&res, &req, sock);
-	route_dispatch(&res, &req);
-	http_flush(&res);
-	http_close(&res);
-	http_free_request(&req);
-	close(sock);
+
+	client_ev(sock, POLLIN, clt);
 	return;
 }