Commit Diff


commit - 6f4eae69642d0a78da926cf32b3639e883f5fe09
commit + 37f7dbf5f09ff1eb1e37d380b32533e876325ef2
blob - 58a88e7f43f9c77ff11bef8420d7ec926c4ffb56
blob + 2446e2dde7afc86c2e450ef5c9845b2bd569261f
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -25,6 +25,7 @@
 #include <errno.h>
 #include <event.h>
 #include <imsg.h>
+#include <signal.h>
 #include <stdarg.h>
 #include <stdlib.h>
 #include <stdio.h>
@@ -41,10 +42,14 @@
 #include "log.h"
 #include "tmpl.h"
 
-size_t	 fcgi_parse_record(uint8_t *, size_t, struct request *);
-void	 fcgi_parse_begin_request(uint8_t *, uint16_t, struct request *,
+static void	 fcgi_sighdlr(int, short, void *);
+static void	 fcgi_shutdown(void);
+static void	 fcgi_launch(struct gotwebd *);
+
+void	 fcgi_parse_record(struct gotwebd_fcgi_record *);
+int	 fcgi_parse_begin_request(uint8_t *, uint16_t, struct request *,
 	    uint16_t);
-void	 fcgi_parse_params(uint8_t *, uint16_t, struct request *, uint16_t);
+int	 fcgi_parse_params(uint8_t *, uint16_t, struct gotwebd_fcgi_params *);
 int	 fcgi_send_response(struct request *, int, const void *, size_t);
 
 void	 dump_fcgi_request_body(const char *, struct fcgi_record_header *);
@@ -54,205 +59,127 @@ void	 dump_fcgi_begin_request_body(const char *,
 void	 dump_fcgi_end_request_body(const char *,
 	    struct fcgi_end_request_body *);
 
-extern int	 cgi_inflight;
 extern struct requestlist requests;
 
-void
-fcgi_request(int fd, short events, void *arg)
+static void
+fcgi_shutdown(void)
 {
-	struct request *c = arg;
-	ssize_t n;
-	size_t parsed = 0;
-
-	n = read(fd, c->buf + c->buf_pos + c->buf_len,
-	    FCGI_RECORD_SIZE - c->buf_pos - c->buf_len);
-
-	switch (n) {
-	case -1:
-		switch (errno) {
-		case EINTR:
-		case EAGAIN:
-			event_add(&c->ev, NULL);
-			return;
-		default:
-			goto fail;
-		}
-		break;
-	case 0:
-		if (c->client_status == CLIENT_CONNECT) {
-			log_warnx("client %u closed connection too early",
-			    c->request_id);
-			goto fail;
-		}
-		return;
-	default:
-		break;
+	imsgbuf_clear(&gotwebd_env->iev_parent->ibuf);
+	free(gotwebd_env->iev_parent);
+	if (gotwebd_env->iev_server) {
+		imsgbuf_clear(&gotwebd_env->iev_server->ibuf);
+		free(gotwebd_env->iev_server);
 	}
 
-	c->buf_len += n;
+	free(gotwebd_env);
 
-	/*
-	 * Parse the records as they are received. Per the FastCGI
-	 * specification, the server need only receive the FastCGI
-	 * parameter records in full; it is free to begin execution
-	 * at that point, which is what happens here.
-	 */
-	do {
-		parsed = fcgi_parse_record(c->buf + c->buf_pos, c->buf_len, c);
-
-		/*
-		 * When we start to actually process the entry, we
-		 * send the request to the gotweb process, so we're
-		 * done.
-		 */
-		if (c->client_status == CLIENT_REQUEST) {
-			fcgi_cleanup_request(c);
-			return;
-		}
-
-		if (parsed != 0) {
-			c->buf_pos += parsed;
-			c->buf_len -= parsed;
-		}
-
-		/* drop the parsed record */
-		if (parsed != 0 && c->buf_len > 0) {
-			memmove(c->buf, c->buf + c->buf_pos, c->buf_len);
-			c->buf_pos = 0;
-		}
-	} while (parsed > 0 && c->buf_len > 0);
-
-	event_add(&c->ev, NULL);
-	return;
-fail:
-	fcgi_cleanup_request(c);
+	exit(0);
 }
 
-size_t
-fcgi_parse_record(uint8_t *buf, size_t n, struct request *c)
+static void
+fcgi_sighdlr(int sig, short event, void *arg)
 {
-	struct fcgi_record_header *h;
-
-	if (n < sizeof(struct fcgi_record_header))
-		return 0;
-
-	h = (struct fcgi_record_header*) buf;
-
-	dump_fcgi_record_header("", h);
-
-	if (n < sizeof(struct fcgi_record_header) + ntohs(h->content_len)
-	    + h->padding_len)
-		return 0;
-
-	dump_fcgi_request_body("", h);
-
-	if (h->version != 1)
-		log_warn("wrong version");
-
-	switch (h->type) {
-	case FCGI_BEGIN_REQUEST:
-		fcgi_parse_begin_request(buf +
-		    sizeof(struct fcgi_record_header),
-		    ntohs(h->content_len), c, ntohs(h->id));
+	switch (sig) {
+	case SIGHUP:
+		log_info("%s: ignoring SIGHUP", __func__);
 		break;
-	case FCGI_PARAMS:
-		fcgi_parse_params(buf + sizeof(struct fcgi_record_header),
-		    ntohs(h->content_len), c, ntohs(h->id));
+	case SIGPIPE:
+		log_info("%s: ignoring SIGPIPE", __func__);
 		break;
-	case FCGI_STDIN:
-		return 0;
-	case FCGI_ABORT_REQUEST:
-		fcgi_create_end_record(c);
-		fcgi_cleanup_request(c);
-		return 0;
+	case SIGUSR1:
+		log_info("%s: ignoring SIGUSR1", __func__);
+		break;
+	case SIGCHLD:
+		break;
+	case SIGINT:
+	case SIGTERM:
+		fcgi_shutdown();
+		break;
 	default:
-		log_warn("unimplemented type %d", h->type);
+		log_warn("unexpected signal %d", sig);
 		break;
 	}
-
-	return (sizeof(struct fcgi_record_header) + ntohs(h->content_len)
-	    + h->padding_len);
 }
 
-void
-fcgi_parse_begin_request(uint8_t *buf, uint16_t n,
-    struct request *c, uint16_t id)
+static void
+send_parsed_params(struct gotwebd_fcgi_params *params)
 {
-	/* XXX -- FCGI_CANT_MPX_CONN */
-	if (c->request_started) {
-		log_warn("unexpected FCGI_BEGIN_REQUEST, ignoring");
-		return;
-	}
+	struct gotwebd *env = gotwebd_env;
 
-	if (n != sizeof(struct fcgi_begin_request_body)) {
-		log_warn("wrong size %d != %lu", n,
-		    sizeof(struct fcgi_begin_request_body));
-		return;
-	}
-
-	c->request_started = 1;
-	c->id = id;
+	if (imsg_compose_event(env->iev_server, GOTWEBD_IMSG_FCGI_PARAMS,
+	    GOTWEBD_PROC_SERVER, -1, -1, params, sizeof(*params)) == -1)
+		log_warn("imsg_compose_event");
 }
 
 static void
-process_request(struct request *c)
+abort_request(uint32_t request_id)
 {
 	struct gotwebd *env = gotwebd_env;
-	struct imsgev *iev_gotweb;
-	int ret, i;
-	struct request ic;
 
-	memcpy(&ic, c, sizeof(ic));
-
-	/* Don't leak pointers from our address space to another process. */
-	ic.sock = NULL;
-	ic.srv = NULL;
-	ic.t = NULL;
-	ic.tp = NULL;
-	ic.buf = NULL;
-	ic.outbuf = NULL;
-
-	/* Other process will use its own set of temp files. */
-	for (i = 0; i < nitems(c->priv_fd); i++)
-		ic.priv_fd[i] = -1;
-	ic.fd = -1;
-
-	/* Round-robin requests across gotweb processes. */
-	iev_gotweb = &env->iev_gotweb[env->gotweb_cur];
-	env->gotweb_cur = (env->gotweb_cur + 1) % env->prefork;
-
-	ret = imsg_compose_event(iev_gotweb, GOTWEBD_IMSG_REQ_PROCESS,
-	    GOTWEBD_PROC_SERVER, -1, c->fd, &ic, sizeof(ic));
-	if (ret == -1) {
+	if (imsg_compose_event(env->iev_server, GOTWEBD_IMSG_REQ_ABORT,
+	    GOTWEBD_PROC_SERVER, -1, -1, &request_id, sizeof(request_id)) == -1)
 		log_warn("imsg_compose_event");
-		return;
-	}
-	c->fd = -1;
-
-	c->client_status = CLIENT_REQUEST;
 }
 
 void
-fcgi_parse_params(uint8_t *buf, uint16_t n, struct request *c, uint16_t id)
+fcgi_parse_record(struct gotwebd_fcgi_record *rec)
 {
-	uint32_t name_len, val_len;
-	uint8_t *val;
+	struct fcgi_record_header *h;
+	uint8_t *record_body;
+	struct gotwebd_fcgi_params params = { 0 };
 
-	if (!c->request_started) {
-		log_warn("FCGI_PARAMS without FCGI_BEGIN_REQUEST, ignoring");
+	if (rec->record_len < sizeof(struct fcgi_record_header) ||
+	    rec->record_len > sizeof(rec->record)) {
+		log_warnx("invalid fcgi record size");
+		abort_request(rec->request_id);
 		return;
 	}
 
-	if (c->id != id) {
-		log_warn("unexpected id, ignoring");
+	h = (struct fcgi_record_header *)&rec->record[0];
+
+	dump_fcgi_record_header("", h);
+
+	if (rec->record_len != sizeof(*h) + ntohs(h->content_len) +
+	    h->padding_len) {
+		abort_request(rec->request_id);
 		return;
 	}
 
-	if (n == 0) {
-		process_request(c);
+	dump_fcgi_request_body("", h);
+
+	if (h->version != 1) {
+		log_warn("wrong fcgi header version: %u", h->version);
+		abort_request(rec->request_id);
 		return;
 	}
 
+	record_body = &rec->record[sizeof(*h)];
+	switch (h->type) {
+	case FCGI_PARAMS:
+		if (fcgi_parse_params(record_body,
+		    ntohs(h->content_len), &params) == -1) {
+			abort_request(rec->request_id);
+			break;
+		}
+		params.request_id = rec->request_id;
+		send_parsed_params(&params);
+		break;
+	default:
+		log_warn("unexpected fcgi type %d", h->type);
+		abort_request(rec->request_id);
+		break;
+	}
+}
+
+int
+fcgi_parse_params(uint8_t *buf, uint16_t n, struct gotwebd_fcgi_params *params)
+{
+	uint32_t name_len, val_len;
+	uint8_t *val;
+
+	if (n == 0)
+		return 0;
+
 	while (n > 0) {
 		if (buf[0] >> 7 == 0) {
 			name_len = buf[0];
@@ -265,11 +192,11 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct req
 				n -= 4;
 				buf += 4;
 			} else
-				return;
+				return -1;
 		}
 
 		if (n == 0)
-			return;
+			return -1;
 
 		if (buf[0] >> 7 == 0) {
 			val_len = buf[0];
@@ -283,50 +210,45 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct req
 				n -= 4;
 				buf += 4;
 			} else
-				return;
+				return -1;
 		}
 
 		if (n < name_len + val_len)
-			return;
+			return -1;
 
 		val = buf + name_len;
 
 		if (val_len < MAX_QUERYSTRING &&
 		    name_len == 12 &&
 		    strncmp(buf, "QUERY_STRING", 12) == 0) {
-			memcpy(c->querystring, val, val_len);
-			c->querystring[val_len] = '\0';
+			/* TODO: parse querystring here */
+			memcpy(params->querystring, val, val_len);
+			params->querystring[val_len] = '\0';
 		}
 
 		if (val_len < MAX_DOCUMENT_URI &&
 		    name_len == 12 &&
 		    strncmp(buf, "DOCUMENT_URI", 12) == 0) {
-			memcpy(c->document_uri, val, val_len);
-			c->document_uri[val_len] = '\0';
+			memcpy(params->document_uri, val, val_len);
+			params->document_uri[val_len] = '\0';
 		}
 
 		if (val_len < MAX_SERVER_NAME &&
 		    name_len == 11 &&
 		    strncmp(buf, "SERVER_NAME", 11) == 0) {
-			memcpy(c->server_name, val, val_len);
-			c->server_name[val_len] = '\0';
+			memcpy(params->server_name, val, val_len);
+			params->server_name[val_len] = '\0';
 		}
 
 		if (name_len == 5 &&
 		    strncmp(buf, "HTTPS", 5) == 0)
-			c->https = 1;
+			params->https = 1;
 
 		buf += name_len + val_len;
 		n -= name_len - val_len;
 	}
-}
 
-void
-fcgi_timeout(int fd, short events, void *arg)
-{
-	struct request *c = arg;
-	log_warnx("request %u has timed out", c->request_id);
-	fcgi_cleanup_request((struct request*) arg);
+	return 0;
 }
 
 static int
@@ -458,8 +380,6 @@ fcgi_create_end_record(struct request *c)
 void
 fcgi_cleanup_request(struct request *c)
 {
-	cgi_inflight--;
-
 	if (evtimer_initialized(&c->tmo))
 		evtimer_del(&c->tmo);
 	if (event_initialized(&c->ev))
@@ -514,4 +434,221 @@ dump_fcgi_end_request_body(const char *p, struct fcgi_
 {
 	log_debug("%sappStatus:       %d", p, ntohl(b->app_status));
 	log_debug("%sprotocolStatus:  %d", p, b->protocol_status);
+}
+
+static void
+fcgi_launch(struct gotwebd *env)
+{
+	if (env->iev_server == NULL)
+		fatalx("server process not connected");
+#ifndef PROFILE
+	if (pledge("stdio", NULL) == -1)
+		fatal("pledge");
+#endif
+	event_add(&env->iev_server->ev, NULL);
+}
+
+static struct gotwebd_fcgi_record *
+recv_record(struct imsg *imsg)
+{
+	struct gotwebd_fcgi_record *record;
+
+	record = calloc(1, sizeof(*record));
+	if (record == NULL) {
+		log_warn("calloc");
+		return NULL;
+	}
+
+	if (imsg_get_data(imsg, record, sizeof(*record)) == -1) {
+		log_warn("imsg_get_data");
+		free(record);
+		return NULL;
+	}
+
+	return record;
+}
+
+static void
+fcgi_dispatch_server(int fd, short event, void *arg)
+{
+	struct imsgev		*iev = arg;
+	struct imsgbuf		*ibuf;
+	struct imsg		 imsg;
+	ssize_t			 n;
+	int			 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1)
+			fatal("imsgbuf_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if (imsgbuf_write(ibuf) == -1)
+			fatal("imsgbuf_write");
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("imsg_get");
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_FCGI_PARSE_PARAMS: {
+			struct gotwebd_fcgi_record *rec;
+
+			rec = recv_record(&imsg);
+			if (rec) {
+				fcgi_parse_record(rec);
+				free(rec);
+			}
+			break;
+		}
+		default:
+			fatalx("%s: unknown imsg type %d", __func__,
+			    imsg.hdr.type);
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+static void
+recv_server_pipe(struct gotwebd *env, struct imsg *imsg)
+{
+	struct imsgev *iev;
+	int fd;
+
+	if (env->iev_server != NULL) {
+		log_warn("server pipe already received");
+		return;
+	}
+
+	fd = imsg_get_fd(imsg);
+	if (fd == -1)
+		fatalx("invalid server pipe fd");
+
+	iev = calloc(1, sizeof(*iev));
+	if (iev == NULL)
+		fatal("calloc");
+
+	if (imsgbuf_init(&iev->ibuf, fd) == -1)
+		fatal("imsgbuf_init");
+	imsgbuf_allow_fdpass(&iev->ibuf);
+	imsgbuf_set_maxsize(&iev->ibuf, sizeof(struct gotwebd_fcgi_record));
+
+	iev->handler = fcgi_dispatch_server;
+	iev->data = iev;
+	event_set(&iev->ev, fd, EV_READ, fcgi_dispatch_server, iev);
+	imsg_event_add(iev);
+
+	env->iev_server = iev;
 }
+
+static void
+fcgi_dispatch_main(int fd, short event, void *arg)
+{
+	struct imsgev		*iev = arg;
+	struct imsgbuf		*ibuf;
+	struct imsg		 imsg;
+	struct gotwebd		*env = gotwebd_env;
+	ssize_t			 n;
+	int			 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1)
+			fatal("imsgbuf_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if (imsgbuf_write(ibuf) == -1)
+			fatal("imsgbuf_write");
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("imsg_get");
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_CFG_DONE:
+			config_getcfg(env, &imsg);
+			break;
+		case GOTWEBD_IMSG_CTL_PIPE:
+			recv_server_pipe(env, &imsg);
+			break;
+		case GOTWEBD_IMSG_CTL_START:
+			fcgi_launch(env);
+			break;
+		default:
+			fatalx("%s: unknown imsg type %d", __func__,
+			    imsg.hdr.type);
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+void
+gotwebd_fcgi(struct gotwebd *env, int fd)
+{
+	struct event	 sighup, sigint, sigusr1, sigchld, sigterm;
+	struct event_base *evb;
+
+	evb = event_init();
+
+	if ((env->iev_parent = malloc(sizeof(*env->iev_parent))) == NULL)
+		fatal("malloc");
+	if (imsgbuf_init(&env->iev_parent->ibuf, fd) == -1)
+		fatal("imsgbuf_init");
+	imsgbuf_allow_fdpass(&env->iev_parent->ibuf);
+	env->iev_parent->handler = fcgi_dispatch_main;
+	env->iev_parent->data = env->iev_parent;
+	event_set(&env->iev_parent->ev, fd, EV_READ, fcgi_dispatch_main,
+	    env->iev_parent);
+	event_add(&env->iev_parent->ev, NULL);
+
+	signal(SIGPIPE, SIG_IGN);
+
+	signal_set(&sighup, SIGHUP, fcgi_sighdlr, env);
+	signal_add(&sighup, NULL);
+	signal_set(&sigint, SIGINT, fcgi_sighdlr, env);
+	signal_add(&sigint, NULL);
+	signal_set(&sigusr1, SIGUSR1, fcgi_sighdlr, env);
+	signal_add(&sigusr1, NULL);
+	signal_set(&sigchld, SIGCHLD, fcgi_sighdlr, env);
+	signal_add(&sigchld, NULL);
+	signal_set(&sigterm, SIGTERM, fcgi_sighdlr, env);
+	signal_add(&sigterm, NULL);
+
+#ifndef PROFILE
+	if (pledge("stdio recvfd", NULL) == -1)
+		fatal("pledge");
+#endif
+	event_dispatch();
+	event_base_free(evb);
+	fcgi_shutdown();
+}
blob - cc891f8eb78a1444fed0684e79e52a496a2008c7
blob + 5f2b1c6836e35e7f9ed602b0e1425472f331e026
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -156,6 +156,18 @@ gotweb_get_socket(int sock_id)
 	return NULL;
 }
 
+static void
+cleanup_request(struct request *c)
+{
+	uint32_t request_id = c->request_id;
+
+	fcgi_cleanup_request(c);
+
+	if (imsg_compose_event(gotwebd_env->iev_server, GOTWEBD_IMSG_REQ_ABORT,
+	    GOTWEBD_PROC_GOTWEB, -1, -1, &request_id, sizeof(request_id)) == -1)
+		log_warn("imsg_compose_event");
+}
+
 static struct request *
 recv_request(struct imsg *imsg)
 {
@@ -211,14 +223,14 @@ recv_request(struct imsg *imsg)
 	c->tp = template(c, fcgi_write, c->outbuf, GOTWEBD_CACHESIZE);
 	if (c->tp == NULL) {
 		log_warn("gotweb init template");
-		fcgi_cleanup_request(c);
+		cleanup_request(c);
 		return NULL;
 	}
 
 	c->sock = gotweb_get_socket(c->sock_id);
 	if (c->sock == NULL) {
 		log_warn("socket id '%d' not found", c->sock_id);
-		fcgi_cleanup_request(c);
+		cleanup_request(c);
 		return NULL;
 	}
 
@@ -226,15 +238,15 @@ recv_request(struct imsg *imsg)
 	error = gotweb_init_transport(&c->t);
 	if (error) {
 		log_warnx("gotweb init transport: %s", error->msg);
-		fcgi_cleanup_request(c);
+		cleanup_request(c);
 		return NULL;
 	}
 
 	/* get the gotwebd server */
-	srv = gotweb_get_server(c->server_name);
+	srv = gotweb_get_server(c->fcgi_params.server_name);
 	if (srv == NULL) {
-		log_warnx("server '%s' not found", c->server_name);
-		fcgi_cleanup_request(c);
+		log_warnx("server '%s' not found", c->fcgi_params.server_name);
+		cleanup_request(c);
 		return NULL;
 	}
 	c->srv = srv;
@@ -262,7 +274,7 @@ gotweb_process_request(struct request *c)
 		goto err;
 	}
 	c->t->qs = qs;
-	error = gotweb_parse_querystring(qs, c->querystring);
+	error = gotweb_parse_querystring(qs, c->fcgi_params.querystring);
 	if (error) {
 		log_warnx("%s: %s", __func__, error->msg);
 		goto err;
@@ -270,22 +282,23 @@ gotweb_process_request(struct request *c)
 
 	/* Log the request. */
 	if (gotwebd_env->gotwebd_verbose > 0) {
+		struct gotwebd_fcgi_params *p = &c->fcgi_params;
 		char *server_name = NULL;
 		char *querystring = NULL;
 		char *document_uri = NULL;
 
-		if (c->server_name[0] &&
-		    stravis(&server_name, c->server_name, VIS_SAFE) == -1) {
+		if (p->server_name[0] &&
+		    stravis(&server_name, p->server_name, VIS_SAFE) == -1) {
 			log_warn("stravis");
 			server_name = NULL;
 		}
-		if (c->querystring[0] &&
-		    stravis(&querystring, c->querystring, VIS_SAFE) == -1) {
+		if (p->querystring[0] &&
+		    stravis(&querystring, p->querystring, VIS_SAFE) == -1) {
 			log_warn("stravis");
 			querystring = NULL;
 		}
-		if (c->document_uri[0] &&
-		    stravis(&document_uri, c->document_uri, VIS_SAFE) == -1) {
+		if (p->document_uri[0] &&
+		    stravis(&document_uri, p->document_uri, VIS_SAFE) == -1) {
 			log_warn("stravis");
 			document_uri = NULL;
 		}
@@ -1160,12 +1173,13 @@ int
 gotweb_render_absolute_url(struct request *c, struct gotweb_url *url)
 {
 	struct template	*tp = c->tp;
-	const char	*proto = c->https ? "https" : "http";
+	struct gotwebd_fcgi_params *p = &c->fcgi_params;
+	const char	*proto = p->https ? "https" : "http";
 
 	if (tp_writes(tp, proto) == -1 ||
 	    tp_writes(tp, "://") == -1 ||
-	    tp_htmlescape(tp, c->server_name) == -1 ||
-	    tp_htmlescape(tp, c->document_uri) == -1)
+	    tp_htmlescape(tp, p->server_name) == -1 ||
+	    tp_htmlescape(tp, p->document_uri) == -1)
 		return -1;
 
 	return gotweb_render_url(c, url);
@@ -1495,6 +1509,7 @@ gotweb_dispatch_server(int fd, short event, void *arg)
 			c = recv_request(&imsg);
 			if (c) {
 				int request_id = c->request_id;
+				log_info("%u: %s: request %u", getpid(), __func__, c->request_id);
 				if (gotweb_process_request(c) == -1) {
 					log_warnx("request %u failed",
 					    request_id);
@@ -1506,7 +1521,7 @@ gotweb_dispatch_server(int fd, short event, void *arg)
 				}
 
 				fcgi_create_end_record(c);
-				fcgi_cleanup_request(c);
+				cleanup_request(c);
 			}
 			break;
 		default:
blob - 68ffcc1638c3db490e1b14e4ab1194f8dc7776db
blob + 361e94d9fc08626854f1c0e4d31af3be36bec935
--- gotwebd/gotwebd.c
+++ gotwebd/gotwebd.c
@@ -53,6 +53,7 @@ void	 gotwebd_configure_done(struct gotwebd *);
 void	 gotwebd_sighdlr(int sig, short event, void *arg);
 void	 gotwebd_shutdown(void);
 void	 gotwebd_dispatch_server(int, short, void *);
+void	 gotwebd_dispatch_fcgi(int, short, void *);
 void	 gotwebd_dispatch_gotweb(int, short, void *);
 
 struct gotwebd	*gotwebd_env;
@@ -156,6 +157,56 @@ sockets_compose_main(struct gotwebd *env, uint32_t typ
 
 void
 gotwebd_dispatch_server(int fd, short event, void *arg)
+{
+	struct imsgev		*iev = arg;
+	struct imsgbuf		*ibuf;
+	struct imsg		 imsg;
+	struct gotwebd		*env = gotwebd_env;
+	ssize_t			 n;
+	int			 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1)
+			fatal("imsgbuf_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if (imsgbuf_write(ibuf) == -1)
+			fatal("imsgbuf_write");
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("imsg_get");
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_CFG_DONE:
+			gotwebd_configure_done(env);
+			break;
+		default:
+			fatalx("%s: unknown imsg type %d", __func__,
+			    imsg.hdr.type);
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
+}
+
+void
+gotwebd_dispatch_fcgi(int fd, short event, void *arg)
 {
 	struct imsgev		*iev = arg;
 	struct imsgbuf		*ibuf;
@@ -320,6 +371,9 @@ spawn_process(struct gotwebd *env, const char *argv0, 
 		if (asprintf(&s, "-S%d", env->prefork) == -1)
 			fatal("asprintf");
 		argv[argc++] = s;
+	} else if (proc_type == GOTWEBD_PROC_FCGI) {
+		argv[argc++] = "-F";
+		argv[argc++] = username;
 	} else if (proc_type == GOTWEBD_PROC_GOTWEB) {
 		char *s;
 
@@ -388,7 +442,7 @@ main(int argc, char **argv)
 		fatal("%s: calloc", __func__);
 	config_init(env);
 
-	while ((ch = getopt(argc, argv, "D:dG:f:nS:vW:")) != -1) {
+	while ((ch = getopt(argc, argv, "D:dG:f:F:nS:vW:")) != -1) {
 		switch (ch) {
 		case 'D':
 			if (cmdline_symset(optarg) < 0)
@@ -409,6 +463,10 @@ main(int argc, char **argv)
 		case 'f':
 			conffile = optarg;
 			break;
+		case 'F':
+			proc_type = GOTWEBD_PROC_FCGI;
+			gotwebd_username = optarg;
+			break;
 		case 'n':
 			no_action = 1;
 			break;
@@ -487,6 +545,22 @@ main(int argc, char **argv)
 
 		sockets(env, GOTWEBD_SOCK_FILENO);
 		return 1;
+	case GOTWEBD_PROC_FCGI:
+		setproctitle("fcgi");
+		log_procinit("fcgi");
+
+		if (chroot(env->httpd_chroot) == -1)
+			fatal("chroot %s", env->httpd_chroot);
+		if (chdir("/") == -1)
+			fatal("chdir /");
+
+		if (setgroups(gotwebd_ngroups, gotwebd_groups) == -1 ||
+		    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 ||
+		    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
+			fatal("failed to drop privileges");
+
+		gotwebd_fcgi(env, GOTWEBD_SOCK_FILENO);
+		return 1;
 	case GOTWEBD_PROC_GOTWEB:
 		setproctitle("gotweb");
 		log_procinit("gotweb");
@@ -510,6 +584,11 @@ main(int argc, char **argv)
 	env->iev_server = calloc(env->server_cnt, sizeof(*env->iev_server));
 	if (env->iev_server == NULL)
 		fatal("calloc");
+
+	env->iev_fcgi = calloc(env->server_cnt, sizeof(*env->iev_fcgi));
+	if (env->iev_fcgi == NULL)
+		fatal("calloc");
+
 	env->iev_gotweb = calloc(env->prefork, sizeof(*env->iev_gotweb));
 	if (env->iev_gotweb == NULL)
 		fatal("calloc");
@@ -518,12 +597,18 @@ main(int argc, char **argv)
 		spawn_process(env, argv0, &env->iev_server[i],
 		    GOTWEBD_PROC_SERVER, gotwebd_username,
 		    gotwebd_dispatch_server);
+
+		spawn_process(env, argv0, &env->iev_fcgi[i],
+		    GOTWEBD_PROC_FCGI, gotwebd_username,
+		    gotwebd_dispatch_fcgi);
 	}
+
 	for (i = 0; i < env->prefork; ++i) {
 		spawn_process(env, argv0, &env->iev_gotweb[i],
 		    GOTWEBD_PROC_GOTWEB, gotwebd_username,
 		    gotwebd_dispatch_gotweb);
 	}
+
 	if (chdir("/") == -1)
 		fatal("chdir /");
 
@@ -578,26 +663,38 @@ main(int argc, char **argv)
 static void
 connect_children(struct gotwebd *env)
 {
-	struct imsgev *iev1, *iev2;
+	struct imsgev *iev_server, *iev_fcgi, *iev_gotweb;
 	int pipe[2];
 	int i, j;
 
 	for (i = 0; i < env->server_cnt; i++) {
-		iev1 = &env->iev_server[i];
+		iev_server = &env->iev_server[i];
+		iev_fcgi = &env->iev_fcgi[i];
 
+		if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1)
+			fatal("socketpair");
+
+		if (send_imsg(iev_server, GOTWEBD_IMSG_CTL_PIPE, pipe[0],
+		    NULL, 0))
+			fatal("send_imsg");
+
+		if (send_imsg(iev_fcgi, GOTWEBD_IMSG_CTL_PIPE, pipe[1],
+		    NULL, 0))
+			fatal("send_imsg");
+
 		for (j = 0; j < env->prefork; j++) {
-			iev2 = &env->iev_gotweb[j];
+			iev_gotweb = &env->iev_gotweb[j];
 
 			if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC,
 			    pipe) == -1)
 				fatal("socketpair");
 
-			if (send_imsg(iev1, GOTWEBD_IMSG_CTL_PIPE, pipe[0],
-			    NULL, 0))
+			if (send_imsg(iev_server, GOTWEBD_IMSG_CTL_PIPE,
+			    pipe[0], NULL, 0))
 				fatal("send_imsg");
 
-			if (send_imsg(iev2, GOTWEBD_IMSG_CTL_PIPE, pipe[1],
-			    NULL, 0))
+			if (send_imsg(iev_gotweb, GOTWEBD_IMSG_CTL_PIPE,
+			    pipe[1], NULL, 0))
 				fatal("send_imsg");
 		}
 	}
blob - b6e69183db3f973c8495e874e281a672de6f3f4c
blob + 18fe22bae49e8cf885500524c79e5780dd102d28
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -124,6 +124,7 @@ struct got_reflist_head;
 enum gotwebd_proc_type {
 	GOTWEBD_PROC_PARENT,
 	GOTWEBD_PROC_SERVER,
+	GOTWEBD_PROC_FCGI,
 	GOTWEBD_PROC_GOTWEB,
 };
 
@@ -134,6 +135,9 @@ enum imsg_type {
 	GOTWEBD_IMSG_CFG_DONE,
 	GOTWEBD_IMSG_CTL_PIPE,
 	GOTWEBD_IMSG_CTL_START,
+	GOTWEBD_IMSG_FCGI_PARSE_PARAMS,
+	GOTWEBD_IMSG_FCGI_PARAMS,
+	GOTWEBD_IMSG_REQ_ABORT,
 	GOTWEBD_IMSG_REQ_PROCESS,
 };
 
@@ -237,6 +241,20 @@ enum socket_priv_fds {
 	BLOB_FD_1,
 	BLOB_FD_2,
 	PRIV_FDS__MAX,
+};
+
+struct gotwebd_fcgi_record {
+	uint32_t			 request_id;
+	uint8_t				 record[FCGI_RECORD_SIZE];
+	size_t				 record_len;
+};
+
+struct gotwebd_fcgi_params {
+	uint32_t			 request_id;
+	char				 querystring[MAX_QUERYSTRING];
+	char				 document_uri[MAX_DOCUMENT_URI];
+	char				 server_name[MAX_SERVER_NAME];
+	int				 https;
 };
 
 struct template;
@@ -257,17 +275,14 @@ struct request {
 	uint32_t			 request_id;
 
 	uint8_t				 *buf;
-	size_t				 buf_pos;
 	size_t				 buf_len;
 
 	uint8_t				 *outbuf;
 
-	char				 querystring[MAX_QUERYSTRING];
-	char				 document_uri[MAX_DOCUMENT_URI];
-	char				 server_name[MAX_SERVER_NAME];
-	int				 https;
+	struct gotwebd_fcgi_params	 fcgi_params;
+	int				 nparams;
+	int				 nparams_parsed;
 
-	uint8_t				 request_started;
 	int				 client_status;
 };
 TAILQ_HEAD(requestlist, request);
@@ -325,6 +340,9 @@ TAILQ_HEAD(serverlist, server);
 
 enum client_action {
 	CLIENT_CONNECT,
+	CLIENT_FCGI_BEGIN,
+	CLIENT_FCGI_PARAMS,
+	CLIENT_FCGI_STDIN,
 	CLIENT_REQUEST,
 	CLIENT_DISCONNECT,
 };
@@ -367,6 +385,7 @@ struct gotwebd {
 
 	struct imsgev	*iev_parent;
 	struct imsgev	*iev_server;
+	struct imsgev	*iev_fcgi;
 	struct imsgev	*iev_gotweb;
 
 	uint16_t	 prefork;
@@ -501,11 +520,10 @@ int parse_config(const char *, struct gotwebd *);
 int cmdline_symset(char *);
 
 /* fcgi.c */
-void fcgi_request(int, short, void *);
-void fcgi_timeout(int, short, void *);
 void fcgi_cleanup_request(struct request *);
 void fcgi_create_end_record(struct request *);
 int fcgi_write(void *, const void *, size_t);
+void gotwebd_fcgi(struct gotwebd *, int);
 
 /* got_operations.c */
 const struct got_error *got_gotweb_closefile(FILE *);
blob - d5af2ffcb28eb41174cdfe8a016103fbb42d50da
blob + c7e1ac5b684f3f72cb60e8a5260089c8262b08a9
--- gotwebd/pages.tmpl
+++ gotwebd/pages.tmpl
@@ -167,7 +167,7 @@ nextsep(char *s, char **t)
 	struct server		*srv = c->srv;
 	struct querystring	*qs = c->t->qs;
 	struct gotweb_url	 u_path;
-	const char		*prfx = c->document_uri;
+	const char		*prfx = c->fcgi_params.document_uri;
 	const char		*css = srv->custom_css;
 
 	memset(&u_path, 0, sizeof(u_path));
blob - a06e82cff88e1f0b56f21f10175579e66d777ca1
blob + 14e2eca5e33aa5640070e3a4b9ef8634b2c05f64
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -79,12 +79,136 @@ static struct socket *sockets_conf_new_socket(struct g
 
 int cgi_inflight = 0;
 
+/* Request hash table needs some spare room to avoid collisions. */
+struct requestlist requests[GOTWEBD_MAXCLIENTS * 4];
+static SIPHASH_KEY requests_hash_key;
+
+static void
+requests_init(void)
+{
+	int i;
+
+	arc4random_buf(&requests_hash_key, sizeof(requests_hash_key));
+
+	for (i = 0; i < nitems(requests); i++)
+		TAILQ_INIT(&requests[i]);
+}
+
+static uint64_t
+request_hash(uint32_t request_id)
+{
+	return SipHash24(&requests_hash_key, &request_id, sizeof(request_id));
+}
+
+static void
+add_request(struct request *c)
+{
+	uint64_t slot = request_hash(c->request_id) % nitems(requests);
+	TAILQ_INSERT_HEAD(&requests[slot], c, entry);
+	client_cnt++;
+}
+
+static void
+del_request(struct request *c)
+{
+	uint64_t slot = request_hash(c->request_id) % nitems(requests);
+	TAILQ_REMOVE(&requests[slot], c, entry);
+	client_cnt--;
+}
+
+static struct request *
+find_request(uint32_t request_id)
+{
+	uint64_t slot;
+	struct request *c;
+
+	slot = request_hash(request_id) % nitems(requests);
+	TAILQ_FOREACH(c, &requests[slot], entry) {
+		if (c->request_id == request_id)
+			return c;
+	}
+
+	return NULL;
+}
+
+static void
+cleanup_request(struct request *c)
+{
+	cgi_inflight--;
+
+	del_request(c);
+
+	event_add(&c->sock->ev, NULL);
+
+	if (evtimer_initialized(&c->tmo))
+		evtimer_del(&c->tmo);
+	if (event_initialized(&c->ev))
+		event_del(&c->ev);
+	if (c->fd != -1)
+		close(c->fd);
+	free(c->buf);
+	free(c);
+}
+
+static void
+request_timeout(int fd, short events, void *arg)
+{
+	struct request *c = arg;
+
+	log_warnx("request %u has timed out", c->request_id);
+	cleanup_request(c);
+}
+
+static void
+requests_purge(void)
+{
+	uint64_t slot;
+	struct request *c;
+
+	for (slot = 0; slot < nitems(requests); slot++) {
+		while (!TAILQ_EMPTY(&requests[slot])) {
+			c = TAILQ_FIRST(&requests[slot]);
+			cleanup_request(c);
+		}
+	}
+}
+
+static uint32_t
+get_request_id(void)
+{
+	int duplicate = 0;
+	uint32_t id;
+
+	do {
+		id = arc4random();
+		duplicate = (find_request(id) != NULL);
+	} while (duplicate || id == 0);
+
+	return id;
+}
+
+static void
+request_done(struct request *c)
+{
+	/*
+	 * If we have not yet handed the client off to gotweb.c we
+	 * must send an FCGI end record ourselves.
+	 */
+	if (c->client_status > CLIENT_CONNECT &&
+	    c->client_status < CLIENT_REQUEST)
+		fcgi_create_end_record(c);
+
+	cleanup_request(c);
+}
+
 void
 sockets(struct gotwebd *env, int fd)
 {
 	struct event	 sighup, sigint, sigusr1, sigchld, sigterm;
 	struct event_base *evb;
 
+	requests_init();
+
 	evb = event_init();
 
 	sockets_rlimit(-1);
@@ -198,6 +322,8 @@ sockets_launch(struct gotwebd *env)
 	struct socket *sock;
 	int i, have_unix = 0, have_inet = 0;
 
+	if (env->iev_fcgi == NULL)
+		fatalx("fcgi process not connected");
 	if (env->gotweb_pending != 0)
 		fatal("gotweb process not connected");
 
@@ -250,9 +376,27 @@ sockets_launch(struct gotwebd *env)
 			fatal("pledge");
 	}
 #endif
+	event_add(&env->iev_fcgi->ev, NULL);
 	for (i = 0; i < env->prefork; i++)
 		event_add(&env->iev_gotweb[i].ev, NULL);
+}
 
+static void
+abort_request(struct imsg *imsg)
+{
+	struct request *c;
+	uint32_t request_id;
+
+	if (imsg_get_data(imsg, &request_id, sizeof(request_id)) == -1) {
+		log_warn("imsg_get_data");
+		return;
+	}
+
+	c = find_request(request_id);
+	if (c == NULL)
+		return;
+
+	request_done(c);
 }
 
 static void
@@ -284,6 +428,9 @@ server_dispatch_gotweb(int fd, short event, void *arg)
 			break;
 
 		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_REQ_ABORT:
+			abort_request(&imsg);
+			break;
 		default:
 			fatalx("%s: unknown imsg type %d", __func__,
 			    imsg.hdr.type);
@@ -320,6 +467,7 @@ recv_gotweb_pipe(struct gotwebd *env, struct imsg *ims
 	if (imsgbuf_init(&iev->ibuf, fd) == -1)
 		fatal("imsgbuf_init");
 	imsgbuf_allow_fdpass(&iev->ibuf);
+	imsgbuf_set_maxsize(&iev->ibuf, sizeof(struct gotwebd_fcgi_record));
 
 	iev->handler = server_dispatch_gotweb;
 	iev->data = iev;
@@ -327,9 +475,192 @@ recv_gotweb_pipe(struct gotwebd *env, struct imsg *ims
 	imsg_event_add(iev);
 
 	env->gotweb_pending--;
+}
+
+static int
+process_request(struct request *c)
+{
+	struct gotwebd *env = gotwebd_env;
+	struct imsgev *iev_gotweb;
+	int ret, i;
+	struct request ic;
+
+	memcpy(&ic, c, sizeof(ic));
+
+	/* Don't leak pointers from our address space to another process. */
+	ic.sock = NULL;
+	ic.srv = NULL;
+	ic.t = NULL;
+	ic.tp = NULL;
+	ic.buf = NULL;
+	ic.outbuf = NULL;
+
+	/* Other process will use its own set of temp files. */
+	for (i = 0; i < nitems(c->priv_fd); i++)
+		ic.priv_fd[i] = -1;
+	ic.fd = -1;
+
+	/* Round-robin requests across gotweb processes. */
+	iev_gotweb = &env->iev_gotweb[env->gotweb_cur];
+	env->gotweb_cur = (env->gotweb_cur + 1) % env->prefork;
+
+	ret = imsg_compose_event(iev_gotweb, GOTWEBD_IMSG_REQ_PROCESS,
+	    GOTWEBD_PROC_SERVER, -1, c->fd, &ic, sizeof(ic));
+	if (ret == -1) {
+		log_warn("imsg_compose_event");
+		return -1;
+	}
+
+	c->fd = -1;
+	c->client_status = CLIENT_REQUEST;
+	return 0;
+}
+
+static void
+recv_parsed_params(struct imsg *imsg)
+{
+	struct gotwebd_fcgi_params params, *p;
+	struct request *c;
+
+	if (imsg_get_data(imsg, &params, sizeof(params)) == -1) {
+		log_warn("imsg_get_data");
+		return;
+	}
+
+	c = find_request(params.request_id);
+	if (c == NULL)
+		return;
+
+	if (c->client_status > CLIENT_FCGI_STDIN)
+		return;
+
+	if (c->client_status < CLIENT_FCGI_PARAMS)
+		goto fail;
+
+	p = &c->fcgi_params;
+
+	if (params.querystring[0] != '\0' &&
+	    strlcpy(p->querystring, params.querystring,
+	    sizeof(p->querystring)) >= sizeof(p->querystring)) {
+		log_warnx("querystring too long");
+		goto fail;
+	}
+
+	if (params.document_uri[0] != '\0' &&
+	    strlcpy(p->document_uri, params.document_uri,
+	    sizeof(p->document_uri)) >= sizeof(p->document_uri)) {
+		log_warnx("document uri too long");
+		goto fail;
+	}
+
+	if (params.server_name[0] != '\0' &&
+	    strlcpy(p->server_name, params.server_name,
+	    sizeof(p->server_name)) >= sizeof(p->server_name)) {
+		log_warnx("server name too long");
+		goto fail;
+	}
+
+	if (params.https && !p->https)
+		p->https = 1;
+
+	c->nparams_parsed++;
+
+	if (c->client_status == CLIENT_FCGI_STDIN &&
+	    c->nparams_parsed >= c->nparams) {
+		if (process_request(c) == -1)
+			goto fail;
+	}
+
+	return;
+fail:
+	request_done(c);
+}
+
+static void
+server_dispatch_fcgi(int fd, short event, void *arg)
+{
+	struct imsgev		*iev = arg;
+	struct imsgbuf		*ibuf;
+	struct imsg		 imsg;
+	ssize_t			 n;
+	int			 shut = 0;
+
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		if ((n = imsgbuf_read(ibuf)) == -1)
+			fatal("imsgbuf_read error");
+		if (n == 0)	/* Connection closed */
+			shut = 1;
+	}
+	if (event & EV_WRITE) {
+		if (imsgbuf_write(ibuf) == -1)
+			fatal("imsgbuf_write");
+	}
+
+	for (;;) {
+		if ((n = imsg_get(ibuf, &imsg)) == -1)
+			fatal("imsg_get");
+		if (n == 0)	/* No more messages. */
+			break;
+
+		switch (imsg.hdr.type) {
+		case GOTWEBD_IMSG_FCGI_PARAMS:
+			recv_parsed_params(&imsg);
+			break;
+		case GOTWEBD_IMSG_REQ_ABORT:
+			abort_request(&imsg);
+			break;
+		default:
+			fatalx("%s: unknown imsg type %d", __func__,
+			    imsg.hdr.type);
+		}
+
+		imsg_free(&imsg);
+	}
+
+	if (!shut)
+		imsg_event_add(iev);
+	else {
+		/* This pipe is dead.  Remove its event handler */
+		event_del(&iev->ev);
+		event_loopexit(NULL);
+	}
 }
 
 static void
+recv_fcgi_pipe(struct gotwebd *env, struct imsg *imsg)
+{
+	struct imsgev *iev;
+	int fd;
+
+	if (env->iev_fcgi != NULL) {
+		log_warn("fcgi pipe already received");
+		return;
+	}
+
+	fd = imsg_get_fd(imsg);
+	if (fd == -1)
+		fatalx("invalid gotweb pipe fd");
+
+	iev = calloc(1, sizeof(*iev));
+	if (iev == NULL)
+		fatal("calloc");
+
+	if (imsgbuf_init(&iev->ibuf, fd) == -1)
+		fatal("imsgbuf_init");
+	imsgbuf_allow_fdpass(&iev->ibuf);
+	imsgbuf_set_maxsize(&iev->ibuf, sizeof(struct gotwebd_fcgi_record));
+
+	iev->handler = server_dispatch_fcgi;
+	iev->data = iev;
+	event_set(&iev->ev, fd, EV_READ, server_dispatch_fcgi, iev);
+	imsg_event_add(iev);
+
+	env->iev_fcgi = iev;
+}
+
+static void
 sockets_dispatch_main(int fd, short event, void *arg)
 {
 	struct imsgev		*iev = arg;
@@ -369,7 +700,10 @@ sockets_dispatch_main(int fd, short event, void *arg)
 			config_getcfg(env, &imsg);
 			break;
 		case GOTWEBD_IMSG_CTL_PIPE:
-			recv_gotweb_pipe(env, &imsg);
+			if (env->iev_fcgi == NULL)
+				recv_fcgi_pipe(env, &imsg);
+			else
+				recv_gotweb_pipe(env, &imsg);
 			break;
 		case GOTWEBD_IMSG_CTL_START:
 			sockets_launch(env);
@@ -446,11 +780,18 @@ sockets_shutdown(void)
 		free(sock);
 	}
 
+	requests_purge();
+
 	imsgbuf_clear(&gotwebd_env->iev_parent->ibuf);
 	free(gotwebd_env->iev_parent);
+
+	imsgbuf_clear(&gotwebd_env->iev_fcgi->ibuf);
+	free(gotwebd_env->iev_fcgi);
+
 	for (i = 0; i < gotwebd_env->prefork; i++)
 		imsgbuf_clear(&gotwebd_env->iev_gotweb[i].ibuf);
 	free(gotwebd_env->iev_gotweb);
+
 	free(gotwebd_env);
 
 	exit(0);
@@ -596,6 +937,158 @@ sockets_accept_paused(int fd, short events, void *arg)
 	struct socket *sock = (struct socket *)arg;
 
 	event_add(&sock->ev, NULL);
+}
+
+static int
+parse_params(struct request *c, uint8_t *record, size_t record_len)
+{
+	struct gotwebd *env = gotwebd_env;
+	struct gotwebd_fcgi_record rec;
+	int ret;
+
+	memset(&rec, 0, sizeof(rec));
+
+	memcpy(rec.record, record, record_len);
+	rec.record_len = record_len;
+	rec.request_id = c->request_id;
+
+	ret = imsg_compose_event(env->iev_fcgi,
+	    GOTWEBD_IMSG_FCGI_PARSE_PARAMS,
+	    GOTWEBD_PROC_SERVER, -1, -1, &rec, sizeof(rec));
+	if (ret == -1)
+		log_warn("imsg_compose_event");
+
+	return ret;
+}
+
+static void
+read_fcgi_records(int fd, short events, void *arg)
+{
+	struct request *c = arg;
+	ssize_t n;
+	struct fcgi_record_header h;
+	size_t record_len;
+
+	n = read(fd, c->buf + c->buf_len, FCGI_RECORD_SIZE - c->buf_len);
+	log_info("%u: %s: request %u, read=%zd", getpid(), __func__, c->request_id, n);
+
+	switch (n) {
+	case -1:
+		switch (errno) {
+		case EINTR:
+		case EAGAIN:
+			goto more;
+		default:
+			goto fail;
+		}
+		break;
+	case 0:
+		if (c->client_status < CLIENT_FCGI_STDIN) {
+			log_warnx("client %u closed connection too early",
+			    c->request_id);
+			goto fail;
+		}
+		return;
+	default:
+		break;
+	}
+
+	c->buf_len += n;
+
+	while (c->buf_len >= sizeof(h)) {
+		memcpy(&h, c->buf, sizeof(h));
+
+		record_len = sizeof(h) + ntohs(h.content_len) + h.padding_len;
+		if (record_len > FCGI_RECORD_SIZE) {
+			log_warnx("FGI record length too large");
+			goto fail;
+		}
+
+		if (c->buf_len < record_len)
+			goto more;
+
+		switch (h.type) {
+		case FCGI_BEGIN_REQUEST:
+			if (c->client_status >= CLIENT_FCGI_BEGIN) {
+				log_warnx("unexpected FCGI_BEGIN_REQUEST");
+				goto fail;
+			}
+
+			if (ntohs(h.content_len) !=
+			    sizeof(struct fcgi_begin_request_body)) {
+				log_warnx("wrong begin request size %u != %zu",
+				    ntohs(h.content_len),
+				    sizeof(struct fcgi_begin_request_body));
+				goto fail;
+			}
+
+			/* XXX -- FCGI_CANT_MPX_CONN */
+			c->client_status = CLIENT_FCGI_BEGIN;
+			c->id = ntohs(h.id);
+			break;
+		case FCGI_PARAMS:
+			if (c->client_status < CLIENT_FCGI_BEGIN) {
+				log_warnx("FCGI_PARAMS without "
+				    "FCGI_BEGIN_REQUEST");
+				goto fail;
+			}
+			if (c->client_status > CLIENT_FCGI_PARAMS) {
+				log_warnx("FCGI_PARAMS after FCGI_STDIN");
+				goto fail;
+			}
+
+			if (c->id != ntohs(h.id)) {
+				log_warnx("unexpected ID in FCGI header");
+				goto fail;
+			}
+
+			c->client_status = CLIENT_FCGI_PARAMS;
+			c->nparams++;
+
+			if (parse_params(c, c->buf, record_len) == -1)
+				goto fail;
+			break;
+		case FCGI_ABORT_REQUEST:
+			log_warnx("received FCGI_ABORT_REQUEST from client");
+			request_done(c);
+			return;
+		case FCGI_STDIN:
+			if (c->client_status < CLIENT_FCGI_BEGIN) {
+				log_warnx("FCGI_STDIN without "
+				    "FCGI_BEGIN_REQUEST");
+				goto fail;
+			}
+
+			if (c->client_status < CLIENT_FCGI_PARAMS) {
+				log_warnx("FCGI_STDIN without FCGI_PARAMS");
+				goto fail;
+			}
+
+			if (c->id != ntohs(h.id)) {
+				log_warnx("unexpected ID in FCGI header");
+				goto fail;
+			}
+
+			c->client_status = CLIENT_FCGI_STDIN;
+			if (c->nparams_parsed >= c->nparams) {
+				if (process_request(c) == -1)
+					goto fail;
+			}
+			break;
+		default:
+			log_warn("unexpected FCGI type %u", h.type);
+			goto fail;
+		}
+
+		/* drop the parsed record */
+		c->buf_len -= record_len;
+		memmove(c->buf, c->buf + record_len, c->buf_len);
+	}
+more:
+	event_add(&c->ev, NULL);
+	return;
+fail:
+	request_done(c);
 }
 
 void
@@ -609,12 +1102,16 @@ sockets_socket_accept(int fd, short event, void *arg)
 	socklen_t len;
 	int s;
 
+	log_info("%u: %s: %d clients", getpid(), __func__, client_cnt);
+
 	backoff.tv_sec = 1;
 	backoff.tv_usec = 0;
 
-	event_add(&sock->ev, NULL);
-	if (event & EV_TIMEOUT)
+	if (event & EV_TIMEOUT) {
+		event_add(&sock->ev, NULL);
+		log_info("%u: %s: timeout", getpid(), __func__);
 		return;
+	}
 
 	len = sizeof(ss);
 
@@ -626,6 +1123,8 @@ sockets_socket_accept(int fd, short event, void *arg)
 		case EINTR:
 		case EWOULDBLOCK:
 		case ECONNABORTED:
+			log_info("%u: %s: errno %d", getpid(), __func__, errno);
+			event_add(&sock->ev, NULL);
 			return;
 		case EMFILE:
 		case ENFILE:
@@ -639,14 +1138,21 @@ sockets_socket_accept(int fd, short event, void *arg)
 		}
 	}
 
-	if (client_cnt > GOTWEBD_MAXCLIENTS)
-		goto err;
+	if (client_cnt > GOTWEBD_MAXCLIENTS) {
+		cgi_inflight--;
+		close(s);
+		if (c != NULL)
+			free(c);
+		event_add(&sock->ev, NULL);
+		return;
+	}
 
 	c = calloc(1, sizeof(struct request));
 	if (c == NULL) {
 		log_warn("%s: calloc", __func__);
 		close(s);
 		cgi_inflight--;
+		event_add(&sock->ev, NULL);
 		return;
 	}
 
@@ -656,6 +1162,7 @@ sockets_socket_accept(int fd, short event, void *arg)
 		close(s);
 		cgi_inflight--;
 		free(c);
+		event_add(&sock->ev, NULL);
 		return;
 	}
 
@@ -664,23 +1171,18 @@ sockets_socket_accept(int fd, short event, void *arg)
 	c->sock = sock;
 	memcpy(c->priv_fd, gotwebd_env->priv_fd, sizeof(c->priv_fd));
 	c->sock_id = sock->conf.id;
-	c->buf_pos = 0;
 	c->buf_len = 0;
-	c->request_started = 0;
 	c->client_status = CLIENT_CONNECT;
+	c->request_id = get_request_id();
 
-	event_set(&c->ev, s, EV_READ, fcgi_request, c);
+	event_set(&c->ev, s, EV_READ, read_fcgi_records, c);
 	event_add(&c->ev, NULL);
 
-	evtimer_set(&c->tmo, fcgi_timeout, c);
+	evtimer_set(&c->tmo, request_timeout, c);
 	evtimer_add(&c->tmo, &timeout);
 
-	return;
-err:
-	cgi_inflight--;
-	close(s);
-	if (c != NULL)
-		free(c);
+	log_info("%u: %s: add_request %d", getpid(), __func__, c->request_id);
+	add_request(c);
 }
 
 void