"GOT", but the "O" is a cute, smiling pufferfish. Index | Thread | Search

From:
Stefan Sperling <stsp@stsp.name>
Subject:
gotwebd: parse fcgi records under pledge("stdio")
To:
gameoftrees@openbsd.org
Date:
Wed, 3 Sep 2025 16:42:55 +0200

Download raw body.

Thread
This adds another gotwebd process which runs with pledge("stdio")
and moves the fcgi params parsing into this process.

My long-term plan is to also parse the querystring in this process
such that gotweb.c and other processes which we need for supporting
authentication will not have to do any parsing themselves but just
deal with data in structs and fixed-sized buffers.

This diff is not well tested and probably has issues. I mainly want
to see if people think this is going in the right direction. Is it?

This will only compile on -current.
The new imsg_set_maxsize() API in -current makes it easy to pass
larger structures around so we won't need to do manual chunking
even if our request and querystring structs are somewhat large.

(One known issue is that requests aren't terminated properly and need
to time out to be removed in sockets.c, because gotweb.c does not yet
notify sockets.c when a request is finished.)


M  gotwebd/fcgi.c      |  321+  171-
M  gotwebd/gotweb.c    |   14+   12-
M  gotwebd/gotwebd.c   |   47+    6-
M  gotwebd/gotwebd.h   |   26+    6-
M  gotwebd/pages.tmpl  |    1+    1-
M  gotwebd/sockets.c   |  475+    8-

6 files changed, 884 insertions(+), 204 deletions(-)

commit - 0f01ac5d813fe34a941ee33dbb19dba78ddbe966
commit + c22f99fc2d8535e043a5ab37e0deb39d11575c13
blob - 0f92cc1b3d0cdafc2c5d0232698fed51772579fe
blob + 3940b628da0b7fc9748f5051c23296f4a8330d61
--- 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,200 +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;
+	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);
+	}
 
-	n = read(fd, c->buf + c->buf_pos + c->buf_len,
-	    FCGI_RECORD_SIZE - c->buf_pos - c->buf_len);
+	free(gotwebd_env);
 
-	switch (n) {
-	case -1:
-		switch (errno) {
-		case EINTR:
-		case EAGAIN:
-			event_add(&c->ev, NULL);
-			return;
-		default:
-			goto fail;
-		}
+	exit(0);
+}
+
+static void
+fcgi_sighdlr(int sig, short event, void *arg)
+{
+	switch (sig) {
+	case SIGHUP:
+		log_info("%s: ignoring SIGHUP", __func__);
 		break;
-	case 0:
-		if (c->client_status == CLIENT_CONNECT) {
-			log_warnx("client %u closed connection too early",
-			    c->request_id);
-			goto fail;
-		}
-		return;
+	case SIGPIPE:
+		log_info("%s: ignoring SIGPIPE", __func__);
+		break;
+	case SIGUSR1:
+		log_info("%s: ignoring SIGUSR1", __func__);
+		break;
+	case SIGCHLD:
+		break;
+	case SIGINT:
+	case SIGTERM:
+		fcgi_shutdown();
+		break;
 	default:
+		log_warn("unexpected signal %d", sig);
 		break;
 	}
+}
 
-	c->buf_len += n;
+static void
+send_parsed_params(struct gotwebd_fcgi_params *params)
+{
+	struct gotwebd *env = 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);
+	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");
+}
 
-		/*
-		 * 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;
-		}
+static void
+abort_request(uint32_t request_id)
+{
+	struct gotwebd *env = gotwebd_env;
 
-		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);
+	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");
 }
 
-size_t
-fcgi_parse_record(uint8_t *buf, size_t n, struct request *c)
+void
+fcgi_parse_record(struct gotwebd_fcgi_record *rec)
 {
 	struct fcgi_record_header *h;
+	uint8_t *record_body;
+	struct gotwebd_fcgi_params params = { 0 };
 
-	if (n < sizeof(struct fcgi_record_header))
-		return 0;
+	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;
+	}
 
-	h = (struct fcgi_record_header*) buf;
+	h = (struct fcgi_record_header *)&rec->record[0];
 
 	dump_fcgi_record_header("", h);
 
-	if (n < sizeof(struct fcgi_record_header) + ntohs(h->content_len)
-	    + h->padding_len)
-		return 0;
+	if (rec->record_len != sizeof(*h) + ntohs(h->content_len) +
+	    h->padding_len) {
+		abort_request(rec->request_id);
+		return;
+	}
 
 	dump_fcgi_request_body("", h);
 
-	if (h->version != 1)
-		log_warn("wrong version");
+	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_BEGIN_REQUEST:
-		fcgi_parse_begin_request(buf +
-		    sizeof(struct fcgi_record_header),
-		    ntohs(h->content_len), c, ntohs(h->id));
-		break;
 	case FCGI_PARAMS:
-		fcgi_parse_params(buf + sizeof(struct fcgi_record_header),
-		    ntohs(h->content_len), c, ntohs(h->id));
+		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;
-	case FCGI_STDIN:
-		return 0;
-	case FCGI_ABORT_REQUEST:
-		fcgi_create_end_record(c);
-		fcgi_cleanup_request(c);
-		return 0;
 	default:
-		log_warn("unimplemented type %d", h->type);
+		log_warn("unexpected fcgi type %d", h->type);
+		abort_request(rec->request_id);
 		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)
+int
+fcgi_parse_params(uint8_t *buf, uint16_t n, struct gotwebd_fcgi_params *params)
 {
-	/* XXX -- FCGI_CANT_MPX_CONN */
-	if (c->request_started) {
-		log_warn("unexpected FCGI_BEGIN_REQUEST, ignoring");
-		return;
-	}
-
-	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;
-}
-
-static void
-process_request(struct request *c)
-{
-	struct gotwebd *env = gotwebd_env;
-	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;
-
-	ret = imsg_compose_event(env->iev_gotweb, GOTWEBD_IMSG_REQ_PROCESS,
-	    GOTWEBD_PROC_SERVER, -1, c->fd, &ic, sizeof(ic));
-	if (ret == -1) {
-		log_warn("imsg_compose_event");
-		close(c->fd);
-	}
-	c->fd = -1;
-
-	c->client_status = CLIENT_REQUEST;
-}
-
-void
-fcgi_parse_params(uint8_t *buf, uint16_t n, struct request *c, uint16_t id)
-{
 	uint32_t name_len, val_len;
 	uint8_t *val;
 
-	if (!c->request_started) {
-		log_warn("FCGI_PARAMS without FCGI_BEGIN_REQUEST, ignoring");
-		return;
-	}
+	if (n == 0)
+		return 0;
 
-	if (c->id != id) {
-		log_warn("unexpected id, ignoring");
-		return;
-	}
-
-	if (n == 0) {
-		process_request(c);
-		return;
-	}
-
 	while (n > 0) {
 		if (buf[0] >> 7 == 0) {
 			name_len = buf[0];
@@ -260,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];
@@ -278,42 +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;
 	}
+
+	return 0;
 }
 
 void
@@ -453,8 +388,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))
@@ -510,3 +443,220 @@ 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 - 1d2b63de026c502f7440177d20c1b3e81c019ae6
blob + a62d5200b2cda89b3d6c162c9ecea8bddc684694
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -231,9 +231,9 @@ recv_request(struct imsg *imsg)
 	}
 
 	/* 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);
+		log_warnx("server '%s' not found", c->fcgi_params.server_name);
 		fcgi_cleanup_request(c);
 		return NULL;
 	}
@@ -262,7 +262,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 +270,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 +1161,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);
blob - fb4dfe221b2b5e116cca94b8c78590b4759e2327
blob + 04e40320ff52b4f03be9eed163eed1a14468498a
--- gotwebd/gotwebd.c
+++ gotwebd/gotwebd.c
@@ -315,6 +315,9 @@ spawn_process(struct gotwebd *env, const char *argv0, 
 	if (proc_type == GOTWEBD_PROC_SERVER) {
 		argv[argc++] = "-S";
 		argv[argc++] = username;
+	} else if (proc_type == GOTWEBD_PROC_FCGI) {
+		argv[argc++] = "-F";
+		argv[argc++] = username;
 	} else if (proc_type == GOTWEBD_PROC_GOTWEB) {
 		argv[argc++] = "-G";
 		argv[argc++] = username;
@@ -378,7 +381,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)
@@ -395,6 +398,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;
@@ -469,6 +476,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");
@@ -493,6 +516,9 @@ main(int argc, char **argv)
 	env->iev_server = calloc(env->nserver, sizeof(*env->iev_server));
 	if (env->iev_server == NULL)
 		fatal("calloc");
+	env->iev_fcgi = calloc(env->nserver, sizeof(*env->iev_fcgi));
+	if (env->iev_fcgi == NULL)
+		fatal("calloc");
 	env->iev_gotweb = calloc(env->nserver, sizeof(*env->iev_gotweb));
 	if (env->iev_gotweb == NULL)
 		fatal("calloc");
@@ -501,6 +527,9 @@ 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_gotweb);
 		spawn_process(env, argv0, &env->iev_gotweb[i],
 		    GOTWEBD_PROC_GOTWEB, gotwebd_username,
 		    gotwebd_dispatch_gotweb);
@@ -559,22 +588,34 @@ 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;
 
 	for (i = 0; i < env->nserver; i++) {
-		iev1 = &env->iev_server[i];
-		iev2 = &env->iev_gotweb[i];
+		iev_server = &env->iev_server[i];
+		iev_fcgi = &env->iev_fcgi[i];
+		iev_gotweb = &env->iev_gotweb[i];
 
 		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(iev_fcgi, GOTWEBD_IMSG_CTL_PIPE,
+		    pipe[1], NULL, 0))
+			fatal("send_imsg");
 
-		if (send_imsg(iev2, GOTWEBD_IMSG_CTL_PIPE, pipe[1], NULL, 0))
+		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_gotweb, GOTWEBD_IMSG_CTL_PIPE,
+		    pipe[1], NULL, 0))
+			fatal("send_imsg");
 	}
 }
 
blob - 888f71c5b60cbc3697f03efa568436627738f7dc
blob + 818dc5bf6ace697099163b2e3296de15089a872c
--- 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,
 };
 
@@ -239,6 +243,20 @@ enum socket_priv_fds {
 	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;
 struct request {
 	TAILQ_ENTRY(request)		entry;
@@ -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;
 	size_t		 nserver;
 
@@ -506,6 +525,7 @@ 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 - 0ddf1e382eb72e1f455b15c2ed1acf5408748f83
blob + a5c7c36b416770bf81e52f186e570e83ce9ed64d
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -79,12 +79,125 @@ 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
+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]);
+			fcgi_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
+cleanup_request(struct request *c)
+{
+	cgi_inflight--;
+
+	del_request(c);
+
+	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_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);
@@ -189,8 +302,10 @@ sockets_launch(struct gotwebd *env)
 {
 	struct socket *sock;
 
+	if (env->iev_fcgi == NULL)
+		fatalx("fcgi process not connected");
 	if (env->iev_gotweb == NULL)
-		fatal("gotweb process not connected");
+		fatalx("gotweb process not connected");
 
 	TAILQ_FOREACH(sock, &gotwebd_env->sockets, entry) {
 		log_info("%s: configuring socket %d (%d)", __func__,
@@ -212,7 +327,7 @@ sockets_launch(struct gotwebd *env)
 	if (pledge("stdio inet sendfd", NULL) == -1)
 		fatal("pledge");
 #endif
-	event_add(&env->iev_gotweb->ev, NULL);
+	event_add(&env->iev_fcgi->ev, NULL);
 
 }
 
@@ -284,6 +399,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;
@@ -293,7 +409,202 @@ recv_gotweb_pipe(struct gotwebd *env, struct imsg *ims
 	env->iev_gotweb = iev;
 }
 
+static int
+process_request(struct request *c)
+{
+	struct gotwebd *env = gotwebd_env;
+	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;
+
+	ret = imsg_compose_event(env->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
+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
+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;
@@ -333,7 +644,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);
@@ -400,10 +714,12 @@ sockets_shutdown(void)
 		free(h);
 	}
 
+	requests_purge();
+
 	imsgbuf_clear(&gotwebd_env->iev_parent->ibuf);
 	free(gotwebd_env->iev_parent);
-	imsgbuf_clear(&gotwebd_env->iev_gotweb->ibuf);
-	free(gotwebd_env->iev_gotweb);
+	imsgbuf_clear(&gotwebd_env->iev_fcgi->ibuf);
+	free(gotwebd_env->iev_fcgi);
 	free(gotwebd_env);
 
 	exit(0);
@@ -562,6 +878,157 @@ sockets_accept_paused(int fd, short events, void *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);
+
+	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
 sockets_socket_accept(int fd, short event, void *arg)
 {
@@ -628,17 +1095,17 @@ 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_add(&c->tmo, &timeout);
 
+	add_request(c);
 	return;
 err:
 	cgi_inflight--;