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

From:
Stefan Sperling <stsp@stsp.name>
Subject:
Re: gotwebd fcgi/gotweb process split
To:
gameoftrees@openbsd.org
Date:
Fri, 11 Apr 2025 16:43:40 +0200

Download raw body.

Thread
On Thu, Apr 10, 2025 at 10:52:59AM +0200, Stefan Sperling wrote:
> On Wed, Apr 09, 2025 at 06:31:42PM +0200, Stefan Sperling wrote:
> > This patch is still work-in-progress. It lacks documentation updates,
> > and one TODO is mentioned in the code.
> 
> updated patch which resolves remaining TODOs and updates docs, too.

Another update which fixes the two issues found by me so far:

1) We need to do polling writes in fcgi_forward to avoid:
  fcgi_forward_response: wrote -1 of 1024 bytes: \
    Resource temporarily unavailable

2) fix hang during '/etc/rc.d/gotwebd restart', causing rc to kill gotwebd


M  gotwebd/Makefile                             |    0+   2-
M  gotwebd/Makefile.inc                         |    1+   1-
M  gotwebd/config.c                             |    9+  15-
M  gotwebd/fcgi.c                               |  104+   7-
M  gotwebd/gotweb.c                             |  384+  14-
M  gotwebd/gotwebd.8                            |    8+  10-
M  gotwebd/gotwebd.c                            |  242+  80-
M  gotwebd/gotwebd.conf.5                       |   29+  13-
M  gotwebd/gotwebd.h                            |   30+  10-
D  gotwebd/libexec/Makefile                     |    0+   4-
D  gotwebd/libexec/Makefile.inc                 |    0+  11-
D  gotwebd/libexec/got-read-blob/Makefile       |    0+  15-
D  gotwebd/libexec/got-read-commit/Makefile     |    0+  15-
D  gotwebd/libexec/got-read-gitconfig/Makefile  |    0+  16-
D  gotwebd/libexec/got-read-gotconfig/Makefile  |    0+  19-
D  gotwebd/libexec/got-read-object/Makefile     |    0+  15-
D  gotwebd/libexec/got-read-pack/Makefile       |    0+  16-
D  gotwebd/libexec/got-read-tag/Makefile        |    0+  15-
D  gotwebd/libexec/got-read-tree/Makefile       |    0+  15-
M  gotwebd/parse.y                              |   24+   3-
M  gotwebd/sockets.c                            |  223+  40-
M  regress/gotwebd/Makefile                     |    1+   7-

22 files changed, 1055 insertions(+), 343 deletions(-)

commit - 1fefee5fcecc39a1b50ab7c21b82b751ba1e7d30
commit + 4774f2e2fe6130bd3e9d719dd399e5a25170904c
blob - 6b6e9e1815bb7b6d738165292b522056eee96e22
blob + 545a13d2e5aba6dc9df486f7300af0acf23c4df7
--- gotwebd/Makefile
+++ gotwebd/Makefile
@@ -1,8 +1,6 @@
 .PATH:${.CURDIR}/../lib
 .PATH:${.CURDIR}/../template
 
-SUBDIR = libexec
-
 .include "../got-version.mk"
 .include "Makefile.inc"
 
blob - 8140d7d3e8b25972b9b347173294bd3b671a0da1
blob + 22466be67cc8ab44489af620bed8558a24ed17c9
--- gotwebd/Makefile.inc
+++ gotwebd/Makefile.inc
@@ -1,9 +1,9 @@
 LDADD +=	-lz -lutil
 PREFIX ?=	/usr/local
 BINDIR ?=	${PREFIX}/sbin
+LIBEXECDIR ?=	${PREFIX}/libexec
 CHROOT_DIR ?=	/var/www
 GOTWEB_DIR =	/bin/gotwebd
-LIBEXECDIR =	${GOTWEB_DIR}/libexec
 HTTPD_DIR =	${CHROOT_DIR}/htdocs
 PROG_DIR =	${HTTPD_DIR}/${PROG}
 WWWUSR ?=	www
blob - 980ecf25520da4497a36f585459ebecbf7b1d3f5
blob + 90eb143afce8e50c7ba53d141294ca8616d7a074
--- gotwebd/config.c
+++ gotwebd/config.c
@@ -74,15 +74,6 @@ config_getcfg(struct gotwebd *env, struct imsg *imsg)
 }
 
 int
-config_setserver(struct gotwebd *env, struct server *srv)
-{
-	if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_SRV,
-	    -1, srv, sizeof(*srv)) == -1)
-		fatal("main_compose_sockets GOTWEBD_IMSG_CFG_SRV");
-	return 0;
-}
-
-int
 config_getserver(struct gotwebd *env, struct imsg *imsg)
 {
 	struct server *srv;
@@ -106,17 +97,20 @@ config_getserver(struct gotwebd *env, struct imsg *ims
 }
 
 int
-config_setsock(struct gotwebd *env, struct socket *sock)
+config_setsock(struct gotwebd *env, struct socket *sock, uid_t uid, gid_t gid)
 {
 	/* open listening sockets */
-	if (sockets_privinit(env, sock) == -1)
+	if (sockets_privinit(env, sock, uid, gid) == -1)
 		return -1;
 
 	if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_SOCK, sock->fd,
 	    &sock->conf, sizeof(sock->conf)) == -1)
 		fatal("main_compose_sockets GOTWEBD_IMSG_CFG_SOCK");
 
-	sock->fd = -1;
+	if (main_compose_gotweb(env, GOTWEBD_IMSG_CFG_SOCK, sock->fd,
+	    &sock->conf, sizeof(sock->conf)) == -1)
+		fatal("main_compose_gotweb GOTWEBD_IMSG_CFG_SOCK");
+
 	return 0;
 }
 
@@ -172,13 +166,13 @@ config_setfd(struct gotwebd *env)
 			fd = got_opentempfd();
 			if (fd == -1)
 				fatal("got_opentemp");
-			if (imsg_compose_event(&env->iev_server[j],
+			if (imsg_compose_event(&env->iev_gotweb[j],
 			    GOTWEBD_IMSG_CFG_FD, 0, -1, fd, NULL, 0) == -1)
 				fatal("imsg_compose_event GOTWEBD_IMSG_CFG_FD");
 
-			if (imsgbuf_flush(&env->iev_server[j].ibuf) == -1)
+			if (imsgbuf_flush(&env->iev_gotweb[j].ibuf) == -1)
 				fatal("imsgbuf_flush");
-			imsg_event_add(&env->iev_server[j]);
+			imsg_event_add(&env->iev_gotweb[j]);
 		}
 	}
 
blob - fe58b1f12ab25d401bc5d3c10aaecf29d138f7bd
blob + 3ec4aee067d2084604a4b3ca1fa315b83811ccc3
--- gotwebd/fcgi.c
+++ gotwebd/fcgi.c
@@ -35,6 +35,8 @@
 #include "got_error.h"
 #include "got_reference.h"
 
+#include "got_lib_poll.h"
+
 #include "gotwebd.h"
 #include "log.h"
 #include "tmpl.h"
@@ -53,7 +55,7 @@ void	 dump_fcgi_end_request_body(const char *,
 	    struct fcgi_end_request_body *);
 
 extern int	 cgi_inflight;
-extern volatile int client_cnt;
+extern struct requestlist requests;
 
 void
 fcgi_request(int fd, short events, void *arg)
@@ -142,6 +144,7 @@ fcgi_parse_record(uint8_t *buf, size_t n, struct reque
 		    ntohs(h->content_len), c, ntohs(h->id));
 		break;
 	case FCGI_STDIN:
+		return 0;
 	case FCGI_ABORT_REQUEST:
 		fcgi_create_end_record(c);
 		fcgi_cleanup_request(c);
@@ -175,6 +178,89 @@ fcgi_parse_begin_request(uint8_t *buf, uint16_t n,
 	c->id = id;
 }
 
+static void
+fcgi_forward_response(int fd, short event, void *arg)
+{
+	const struct got_error *err = NULL;
+	struct request *c = arg;
+	uint8_t outbuf[GOTWEBD_CACHESIZE];
+	ssize_t r;
+
+	if ((event & EV_READ) == 0)
+		return;
+
+	r = read(fd, outbuf, sizeof(outbuf));
+	if (r == 0)
+		return;
+
+	if (r == -1) {
+		log_warn("read response");
+	} else {
+		err = got_poll_write_full(c->fd, outbuf, r);
+		if (err) {
+			log_warnx("forward response: %s", err->msg);
+			fcgi_cleanup_request(c);
+			return;
+		}
+	}
+	
+	event_add(c->resp_event, NULL);
+}
+
+static void
+process_request(struct request *c)
+{
+	struct gotwebd *env = gotwebd_env;
+	int ret, i, pipe[2];
+	struct request ic;
+	struct event *resp_event = NULL;
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pipe) == -1) {
+		log_warn("socketpair");
+		return;
+	}
+
+	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;
+	ic.resp_fd = -1;
+
+	resp_event = calloc(1, sizeof(*resp_event));
+	if (resp_event == NULL) {
+		log_warn("calloc");
+		close(pipe[0]);
+		close(pipe[1]);
+		return;
+	}
+
+	ret = imsg_compose_event(env->iev_gotweb, GOTWEBD_IMSG_REQ_PROCESS,
+	    GOTWEBD_PROC_SERVER, getpid(), pipe[0], &ic, sizeof(ic));
+	if (ret == -1) {
+		log_warn("imsg_compose_event");
+		close(pipe[0]);
+		close(pipe[1]);
+		free(resp_event);
+		return;
+	}
+
+	event_set(resp_event, pipe[1], EV_READ, fcgi_forward_response, c);
+	event_add(resp_event, NULL);
+
+	c->resp_fd = pipe[1];
+	c->resp_event = resp_event;
+}
+
 void
 fcgi_parse_params(uint8_t *buf, uint16_t n, struct request *c, uint16_t id)
 {
@@ -192,8 +278,7 @@ fcgi_parse_params(uint8_t *buf, uint16_t n, struct req
 	}
 
 	if (n == 0) {
-		gotweb_process_request(c);
-		template_flush(c->tp);
+		process_request(c);
 		return;
 	}
 
@@ -399,16 +484,28 @@ void
 fcgi_cleanup_request(struct request *c)
 {
 	cgi_inflight--;
-	client_cnt--;
 
-	evtimer_del(&c->tmo);
+	sockets_del_request(c);
+
+	if (evtimer_initialized(&c->tmo))
+		evtimer_del(&c->tmo);
 	if (event_initialized(&c->ev))
 		event_del(&c->ev);
 
-	close(c->fd);
-	template_free(c->tp);
+	if (c->fd != -1)
+		close(c->fd);
+	if (c->resp_fd != -1)
+		close(c->resp_fd);
+	if (c->tp != NULL)
+		template_free(c->tp);
 	if (c->t != NULL)
 		gotweb_free_transport(c->t);
+	if (c->resp_event) {
+		event_del(c->resp_event);
+		free(c->resp_event);
+	}
+	free(c->buf);
+	free(c->outbuf);
 	free(c);
 }
 
blob - 1a1a8770b26108d09399eb43a781fa0e4d63c1b9
blob + 91799176b5f93d034ae4014bd3a97128c31aeee4
--- gotwebd/gotweb.c
+++ gotwebd/gotweb.c
@@ -33,6 +33,7 @@
 #include <imsg.h>
 #include <sha1.h>
 #include <sha2.h>
+#include <signal.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -142,11 +143,124 @@ gotweb_reply_file(struct request *c, const char *ctype
 	return gotweb_reply(c, 200, ctype, NULL);
 }
 
+static void
+free_request(struct request *c)
+{
+	if (c->fd != -1)
+		close(c->fd);
+	if (c->tp != NULL)
+		template_free(c->tp);
+	if (c->t != NULL)
+		gotweb_free_transport(c->t);
+	free(c->buf);
+	free(c->outbuf);
+	free(c);
+}
+
+static struct socket *
+gotweb_get_socket(int sock_id)
+{
+	struct socket *sock;
+
+	TAILQ_FOREACH(sock, &gotwebd_env->sockets, entry) {
+		if (sock->conf.id == sock_id)
+			return sock;
+	}
+
+	return NULL;
+}
+
+static struct request *
+recv_request(struct imsg *imsg)
+{
+	const struct got_error *error;
+	struct request *c;
+	struct server *srv;
+	size_t datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+	int fd = -1;
+	uint8_t *outbuf = NULL;
+
+	if (datalen != sizeof(*c)) {
+		log_warnx("bad request size received over imsg");
+		return NULL;
+	}
+
+	fd = imsg_get_fd(imsg);
+	if (fd == -1) {
+		log_warnx("no client file descriptor");
+		return NULL;
+	}
+
+	c = calloc(1, sizeof(*c));
+	if (c == NULL) {
+		log_warn("calloc");
+		return NULL;
+	}
+
+	outbuf = calloc(1, GOTWEBD_CACHESIZE);
+	if (outbuf == NULL) {
+		log_warn("calloc");
+		free(c);
+		return NULL;
+	}
+
+	memcpy(c, imsg->data, sizeof(*c));
+
+	/* Non-NULL pointers, if any, are not from our address space. */
+	c->sock = NULL;
+	c->srv = NULL;
+	c->t = NULL;
+	c->tp = NULL;
+	c->buf = NULL;
+	c->outbuf = outbuf;
+
+	memset(&c->ev, 0, sizeof(c->ev));
+	memset(&c->tmo, 0, sizeof(c->tmo));
+
+	/* Use our own temporary file descriptors. */
+	memcpy(c->priv_fd, gotwebd_env->priv_fd, sizeof(c->priv_fd));
+
+	c->fd = fd;
+
+	c->tp = template(c, fcgi_write, c->outbuf, GOTWEBD_CACHESIZE);
+	if (c->tp == NULL) {
+		log_warn("gotweb init template");
+		free_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);
+		free_request(c);
+		return NULL;
+	}
+
+	/* init the transport */
+	error = gotweb_init_transport(&c->t);
+	if (error) {
+		log_warnx("gotweb init transport: %s", error->msg);
+		free_request(c);
+		return NULL;
+	}
+
+	/* get the gotwebd server */
+	srv = gotweb_get_server(c->server_name);
+	if (srv == NULL) {
+		log_warnx("server '%s' not found", c->server_name);
+		free_request(c);
+		return NULL;
+	}
+	c->srv = srv;
+
+	return c;
+}
+
 void
 gotweb_process_request(struct request *c)
 {
 	const struct got_error *error = NULL;
-	struct server *srv = NULL;
+	struct server *srv = c->srv;;
 	struct querystring *qs = NULL;
 	struct repo_dir *repo_dir = NULL;
 	struct repo_commit *commit;
@@ -155,19 +269,6 @@ gotweb_process_request(struct request *c)
 	size_t len;
 	int r, binary = 0;
 
-	/* init the transport */
-	error = gotweb_init_transport(&c->t);
-	if (error) {
-		log_warnx("%s: %s", __func__, error->msg);
-		return;
-	}
-	/* get the gotwebd server */
-	srv = gotweb_get_server(c->server_name);
-	if (srv == NULL) {
-		log_warnx("%s: error server is NULL", __func__);
-		goto err;
-	}
-	c->srv = srv;
 	/* parse our querystring */
 	error = gotweb_init_querystring(&qs);
 	if (error) {
@@ -1297,3 +1398,272 @@ gotweb_render_age(struct template *tp, time_t committe
 	}
 	return 0;
 }
+
+static void
+gotweb_shutdown(void)
+{
+	imsgbuf_clear(&gotwebd_env->iev_parent->ibuf);
+	free(gotwebd_env->iev_parent);
+	free(gotwebd_env);
+
+	exit(0);
+}
+
+static void
+gotweb_sighdlr(int sig, short event, void *arg)
+{
+	switch (sig) {
+	case SIGHUP:
+		log_info("%s: ignoring SIGHUP", __func__);
+		break;
+	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:
+		gotweb_shutdown();
+		break;
+	default:
+		log_warn("unhandled signal %d", sig);
+	}
+}
+
+static void
+gotweb_launch(struct gotwebd *env)
+{
+	struct server *srv;
+	const struct got_error *error;
+
+	if (env->iev_server == NULL)
+		fatal("server process not connected");
+
+#ifndef PROFILE
+	if (pledge("stdio rpath recvfd sendfd proc exec unveil", NULL) == -1)
+		fatal("pledge");
+#endif
+
+	TAILQ_FOREACH(srv, &gotwebd_env->servers, entry) {
+		if (unveil(srv->repos_path, "r") == -1)
+			fatal("unveil %s", srv->repos_path);
+	}
+
+	error = got_privsep_unveil_exec_helpers();
+	if (error)
+		fatalx("%s", error->msg);
+
+	if (unveil(NULL, NULL) == -1)
+		fatal("unveil");
+
+	event_add(&env->iev_server->ev, NULL);
+}
+
+static void
+send_request_done(struct imsgev *iev, int request_id)
+{
+	struct gotwebd		*env = gotwebd_env;
+
+	if (imsg_compose_event(env->iev_server, GOTWEBD_IMSG_REQ_DONE,
+	    GOTWEBD_PROC_GOTWEB, getpid(), -1,
+	    &request_id, sizeof(request_id)) == -1)
+		log_warn("imsg_compose_event");
+}
+
+static void
+gotweb_dispatch_server(int fd, short event, void *arg)
+{
+	struct imsgev		*iev = arg;
+	struct imsgbuf		*ibuf;
+	struct imsg		 imsg;
+	struct request		*c;
+	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_REQ_PROCESS:
+			c = recv_request(&imsg);
+			if (c) {
+				int request_id = c->request_id;
+				gotweb_process_request(c);
+				template_flush(c->tp);
+				free_request(c);
+				send_request_done(iev, request_id);
+			}
+			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);
+
+	iev->handler = gotweb_dispatch_server;
+	iev->data = iev;
+	event_set(&iev->ev, fd, EV_READ, gotweb_dispatch_server, iev);
+
+	env->iev_server = iev;
+}
+
+static void
+gotweb_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_SRV:
+			config_getserver(env, &imsg);
+			break;
+		case GOTWEBD_IMSG_CFG_FD:
+			config_getfd(env, &imsg);
+			break;
+		case GOTWEBD_IMSG_CFG_SOCK:
+			config_getsock(env, &imsg);
+			break;
+		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:
+			gotweb_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
+gotweb(struct gotwebd *env, int fd)
+{
+	struct event	 sighup, sigint, sigusr1, sigchld, sigterm;
+	struct event_base *evb;
+
+	evb = event_init();
+
+	sockets_rlimit(-1);
+
+	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 = gotweb_dispatch_main;
+	env->iev_parent->data = env->iev_parent;
+	event_set(&env->iev_parent->ev, fd, EV_READ, gotweb_dispatch_main,
+	    env->iev_parent);
+	event_add(&env->iev_parent->ev, NULL);
+
+	signal(SIGPIPE, SIG_IGN);
+
+	signal_set(&sighup, SIGHUP, gotweb_sighdlr, env);
+	signal_add(&sighup, NULL);
+	signal_set(&sigint, SIGINT, gotweb_sighdlr, env);
+	signal_add(&sigint, NULL);
+	signal_set(&sigusr1, SIGUSR1, gotweb_sighdlr, env);
+	signal_add(&sigusr1, NULL);
+	signal_set(&sigchld, SIGCHLD, gotweb_sighdlr, env);
+	signal_add(&sigchld, NULL);
+	signal_set(&sigterm, SIGTERM, gotweb_sighdlr, env);
+	signal_add(&sigterm, NULL);
+
+#ifndef PROFILE
+	if (pledge("stdio rpath recvfd sendfd proc exec unveil", NULL) == -1)
+		fatal("pledge");
+#endif
+	event_dispatch();
+	event_base_free(evb);
+	gotweb_shutdown();
+}
blob - 8413e78f48a48cfc92e794fff34a97a8765976e7
blob + f6353254aaee1f21a9dd7e56f1aff9a4d95f47af
--- gotwebd/gotwebd.8
+++ gotwebd/gotwebd.8
@@ -85,24 +85,22 @@ can be configured via the
 .Xr gotwebd.conf 5
 configuration file.
 .It
-Git repositories must be created at a suitable location inside the
-web server's
-.Xr chroot 2
-environment.
-These repositories should
+Git repositories must be created.
+These repositories may reside anywhere in the filesystem and must
+be readable, but should
 .Em not
-be writable by the user ID shared between
+be writable, by the user
 .Nm
-and
-.Xr httpd 8 .
+runs as.
 The default location for repositories published by
 .Nm
 is
 .Pa /var/www/got/public .
 .It
-Git repositories served by
+If the Git repositories served by
 .Nm
-should be kept up-to-date with a mechanism such as
+do not receive changes from committers directly, they need to be kept
+up-to-date with a mechanism such as
 .Cm got fetch ,
 .Xr git-fetch 1 ,
 or
blob - 61a08a5b8aa69b071a02be23f9794bab66c345fc
blob + d738c5a52b29d013687fb03ff5f2e55f4a9a512d
--- gotwebd/gotwebd.c
+++ gotwebd/gotwebd.c
@@ -48,11 +48,12 @@
 __dead void usage(void);
 
 int	 main(int, char **);
-int	 gotwebd_configure(struct gotwebd *);
+int	 gotwebd_configure(struct gotwebd *, uid_t, gid_t);
 void	 gotwebd_configure_done(struct gotwebd *);
 void	 gotwebd_sighdlr(int sig, short event, void *arg);
 void	 gotwebd_shutdown(void);
-void	 gotwebd_dispatch_sockets(int, short, void *);
+void	 gotwebd_dispatch_server(int, short, void *);
+void	 gotwebd_dispatch_gotweb(int, short, void *);
 
 struct gotwebd	*gotwebd_env;
 
@@ -75,7 +76,7 @@ imsg_event_add(struct imsgev *iev)
 
 int
 imsg_compose_event(struct imsgev *iev, uint16_t type, uint32_t peerid,
-    pid_t pid, int fd, const void *data, uint16_t datalen)
+    pid_t pid, int fd, const void *data, size_t datalen)
 {
 	int ret;
 
@@ -86,42 +87,67 @@ imsg_compose_event(struct imsgev *iev, uint16_t type, 
 	return (ret);
 }
 
-int
-main_compose_sockets(struct gotwebd *env, uint32_t type, int fd,
-    const void *data, uint16_t len)
+static int
+send_imsg(struct imsgev *iev, uint32_t type, int fd, const void *data,
+    uint16_t len)
 {
-	size_t	 i;
-	int	 ret, d;
+	int	 ret, d = -1;
 
-	for (i = 0; i < env->nserver; ++i) {
+	if (fd != -1 && (d = dup(fd)) == -1)
+		goto err;
+
+	ret = imsg_compose_event(iev, type, 0, -1, d, data, len);
+	if (ret == -1)
+		goto err;
+
+	if (d != -1) {
 		d = -1;
-		if (fd != -1 && (d = dup(fd)) == -1)
+		/* Flush imsg to prevent fd exhaustion. 'd' will be closed. */
+		if (imsgbuf_flush(&iev->ibuf) == -1)
 			goto err;
-
-		ret = imsg_compose_event(&env->iev_server[i], type, 0, -1,
-		    d, data, len);
-		if (ret == -1)
-			goto err;
-
-		/* prevent fd exhaustion */
-		if (d != -1) {
-			if (imsgbuf_flush(&env->iev_server[i].ibuf) == -1)
-				goto err;
-			imsg_event_add(&env->iev_server[i]);
-		}
+		imsg_event_add(iev);
 	}
 
-	if (fd != -1)
-		close(fd);
 	return 0;
-
 err:
-	if (fd != -1)
-		close(fd);
+	if (d != -1)
+		close(d);
 	return -1;
 }
 
 int
+main_compose_sockets(struct gotwebd *env, uint32_t type, int fd,
+    const void *data, uint16_t len)
+{
+	size_t i;
+	int ret = 0;
+
+	for (i = 0; i < env->nserver; ++i) {
+		ret = send_imsg(&env->iev_server[i], type, fd, data, len);
+		if (ret)
+			break;
+	}
+
+	return ret;
+}
+
+int
+main_compose_gotweb(struct gotwebd *env, uint32_t type, int fd,
+    const void *data, uint16_t len)
+{
+	size_t i;
+	int ret = 0;
+
+	for (i = 0; i < env->nserver; ++i) {
+		ret = send_imsg(&env->iev_gotweb[i], type, fd, data, len);
+		if (ret)
+			break;
+	}
+
+	return ret;
+}
+
+int
 sockets_compose_main(struct gotwebd *env, uint32_t type, const void *d,
     uint16_t len)
 {
@@ -129,7 +155,7 @@ sockets_compose_main(struct gotwebd *env, uint32_t typ
 }
 
 void
-gotwebd_dispatch_sockets(int fd, short event, void *arg)
+gotwebd_dispatch_server(int fd, short event, void *arg)
 {
 	struct imsgev		*iev = arg;
 	struct imsgbuf		*ibuf;
@@ -179,6 +205,56 @@ gotwebd_dispatch_sockets(int fd, short event, void *ar
 }
 
 void
+gotwebd_dispatch_gotweb(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_sighdlr(int sig, short event, void *arg)
 {
 	/* struct privsep	*ps = arg; */
@@ -203,10 +279,11 @@ gotwebd_sighdlr(int sig, short event, void *arg)
 	}
 }
 
-static int
-spawn_socket_process(struct gotwebd *env, const char *argv0, int n)
+static void
+spawn_process(struct gotwebd *env, const char *argv0, struct imsgev *iev,
+    enum gotwebd_proc_type proc_type, void (*handler)(int, short, void *))
 {
-	const char	*argv[8];
+	const char	*argv[9];
 	int		 argc = 0;
 	int		 p[2];
 	pid_t		 pid;
@@ -221,21 +298,26 @@ spawn_socket_process(struct gotwebd *env, const char *
 		break;
 	default:	/* parent */
 		close(p[0]);
-		if (imsgbuf_init(&env->iev_server[n].ibuf, p[1]) == -1)
+		if (imsgbuf_init(&iev->ibuf, p[1]) == -1)
 			fatal("imsgbuf_init");
-		imsgbuf_allow_fdpass(&env->iev_server[n].ibuf);
-		env->iev_server[n].handler = gotwebd_dispatch_sockets;
-		env->iev_server[n].data = &env->iev_server[n];
-		event_set(&env->iev_server[n].ev, p[1], EV_READ,
-		    gotwebd_dispatch_sockets, &env->iev_server[n]);
-		event_add(&env->iev_server[n].ev, NULL);
-		return 0;
+		imsgbuf_allow_fdpass(&iev->ibuf);
+		iev->handler = handler;
+		iev->data = iev;
+		event_set(&iev->ev, p[1], EV_READ, handler, iev);
+		event_add(&iev->ev, NULL);
+		return;
 	}
 
 	close(p[1]);
 
 	argv[argc++] = argv0;
-	argv[argc++] = "-S";
+	if (proc_type == GOTWEBD_PROC_SERVER) {
+		argv[argc++] = "-S";
+		argv[argc++] = env->user;
+	} else if (proc_type == GOTWEBD_PROC_GOTWEB) {
+		argv[argc++] = "-G";
+		argv[argc++] = env->user;
+	}
 	if (strcmp(env->gotwebd_conffile, GOTWEBD_CONF) != 0) {
 		argv[argc++] = "-f";
 		argv[argc++] = env->gotwebd_conffile;
@@ -273,12 +355,12 @@ main(int argc, char **argv)
 	struct event		 sigint, sigterm, sighup, sigpipe, sigusr1;
 	struct event_base	*evb;
 	struct gotwebd		*env;
-	struct passwd		*pw;
+	struct passwd		*pw_gotwebd;
 	int			 ch, i;
 	int			 no_action = 0;
-	int			 server_proc = 0;
+	int			 proc_type = GOTWEBD_PROC_PARENT;
 	const char		*conffile = GOTWEBD_CONF;
-	const char		*username = GOTWEBD_DEFAULT_USER;
+	const char		*gotwebd_username = GOTWEBD_DEFAULT_USER;
 	const char		*argv0;
 
 	if ((argv0 = argv[0]) == NULL)
@@ -292,7 +374,7 @@ main(int argc, char **argv)
 		fatal("%s: calloc", __func__);
 	config_init(env);
 
-	while ((ch = getopt(argc, argv, "D:df:nSv")) != -1) {
+	while ((ch = getopt(argc, argv, "D:dG:f:nS:vW:")) != -1) {
 		switch (ch) {
 		case 'D':
 			if (cmdline_symset(optarg) < 0)
@@ -302,6 +384,10 @@ main(int argc, char **argv)
 		case 'd':
 			env->gotwebd_debug = 1;
 			break;
+		case 'G':
+			proc_type = GOTWEBD_PROC_GOTWEB;
+			gotwebd_username = optarg;
+			break;
 		case 'f':
 			conffile = optarg;
 			break;
@@ -309,7 +395,8 @@ main(int argc, char **argv)
 			no_action = 1;
 			break;
 		case 'S':
-			server_proc = 1;
+			proc_type = GOTWEBD_PROC_SERVER;
+			gotwebd_username = optarg;
 			break;
 		case 'v':
 			if (env->gotwebd_verbose < 3)
@@ -327,29 +414,32 @@ main(int argc, char **argv)
 	gotwebd_env = env;
 	env->gotwebd_conffile = conffile;
 
-	if (parse_config(env->gotwebd_conffile, env) == -1)
-		exit(1);
+	if (proc_type == GOTWEBD_PROC_PARENT) {
+		if (parse_config(env->gotwebd_conffile, env) == -1)
+			exit(1);
 
-	if (no_action) {
-		fprintf(stderr, "configuration OK\n");
-		exit(0);
+		if (no_action) {
+			fprintf(stderr, "configuration OK\n");
+			exit(0);
+		}
+
+		if (env->user)
+			gotwebd_username = env->user;
 	}
 
+	pw_gotwebd = getpwnam(gotwebd_username);
+	if (pw_gotwebd == NULL)
+		fatalx("unknown user %s", gotwebd_username);
+
 	/* check for root privileges */
 	if (geteuid())
 		fatalx("need root privileges");
 
-	if (env->user)
-		username = env->user;
-	pw = getpwnam(username);
-	if (pw == NULL)
-		fatalx("unknown user %s", username);
-	env->pw = pw;
-
 	log_init(env->gotwebd_debug, LOG_DAEMON);
 	log_setverbose(env->gotwebd_verbose);
 
-	if (server_proc) {
+	switch (proc_type) {
+	case GOTWEBD_PROC_SERVER:
 		setproctitle("server");
 		log_procinit("server");
 
@@ -357,13 +447,31 @@ main(int argc, char **argv)
 			fatal("chroot %s", env->httpd_chroot);
 		if (chdir("/") == -1)
 			fatal("chdir /");
-		if (setgroups(1, &pw->pw_gid) == -1 ||
-		    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 ||
-		    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
+
+		if (setgroups(1, &pw_gotwebd->pw_gid) == -1 ||
+		    setresgid(pw_gotwebd->pw_gid, pw_gotwebd->pw_gid,
+		    pw_gotwebd->pw_gid) == -1 ||
+		    setresuid(pw_gotwebd->pw_uid, pw_gotwebd->pw_uid,
+		    pw_gotwebd->pw_uid) == -1)
 			fatal("failed to drop privileges");
 
 		sockets(env, GOTWEBD_SOCK_FILENO);
 		return 1;
+	case GOTWEBD_PROC_GOTWEB:
+		setproctitle("gotweb");
+		log_procinit("gotweb");
+
+		if (setgroups(1, &pw_gotwebd->pw_gid) == -1 ||
+		    setresgid(pw_gotwebd->pw_gid, pw_gotwebd->pw_gid,
+		    pw_gotwebd->pw_gid) == -1 ||
+		    setresuid(pw_gotwebd->pw_uid, pw_gotwebd->pw_uid,
+		    pw_gotwebd->pw_uid) == -1)
+			fatal("failed to drop privileges");
+
+		gotweb(env, GOTWEBD_SOCK_FILENO);
+		return 1;
+	default:
+		break;
 	}
 
 	if (!env->gotwebd_debug && daemon(1, 0) == -1)
@@ -375,12 +483,16 @@ main(int argc, char **argv)
 	env->iev_server = calloc(env->nserver, sizeof(*env->iev_server));
 	if (env->iev_server == NULL)
 		fatal("calloc");
+	env->iev_gotweb = calloc(env->nserver, sizeof(*env->iev_gotweb));
+	if (env->iev_gotweb == NULL)
+		fatal("calloc");
 
 	for (i = 0; i < env->nserver; ++i) {
-		if (spawn_socket_process(env, argv0, i) == -1)
-			fatal("spawn_socket_process");
+		spawn_process(env, argv0, &env->iev_server[i],
+		    GOTWEBD_PROC_SERVER, gotwebd_dispatch_server);
+		spawn_process(env, argv0, &env->iev_gotweb[i],
+		    GOTWEBD_PROC_GOTWEB, gotwebd_dispatch_gotweb);
 	}
-
 	if (chdir("/") == -1)
 		fatal("chdir /");
 
@@ -400,12 +512,15 @@ main(int argc, char **argv)
 	signal_add(&sigpipe, NULL);
 	signal_add(&sigusr1, NULL);
 
-	if (gotwebd_configure(env) == -1)
+	if (gotwebd_configure(env,
+	    pw_gotwebd->pw_uid, pw_gotwebd->pw_gid) == -1)
 		fatalx("configuration failed");
 
-	if (setgroups(1, &pw->pw_gid) == -1 ||
-	    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1 ||
-	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
+	if (setgroups(1, &pw_gotwebd->pw_gid) == -1 ||
+	    setresgid(pw_gotwebd->pw_gid, pw_gotwebd->pw_gid,
+	    pw_gotwebd->pw_gid) == -1 ||
+	    setresuid(pw_gotwebd->pw_uid, pw_gotwebd->pw_uid,
+	    pw_gotwebd->pw_uid) == -1)
 		fatal("failed to drop privileges");
 
 #ifdef PROFILE
@@ -432,24 +547,54 @@ main(int argc, char **argv)
 	return (0);
 }
 
+static void
+connect_children(struct gotwebd *env)
+{
+	struct imsgev *iev1, *iev2;
+	int pipe[2];
+	int i;
+
+	for (i = 0; i < env->nserver; i++) {
+		iev1 = &env->iev_server[i];
+		iev2 = &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))
+			fatal("send_imsg");
+
+		if (send_imsg(iev2, GOTWEBD_IMSG_CTL_PIPE, pipe[1], NULL, 0))
+			fatal("send_imsg");
+
+		close(pipe[0]);
+		close(pipe[1]);
+	}
+}
+
 int
-gotwebd_configure(struct gotwebd *env)
+gotwebd_configure(struct gotwebd *env, uid_t uid, gid_t gid)
 {
 	struct server *srv;
 	struct socket *sock;
 
 	/* gotweb need to reload its config. */
-	env->gotwebd_reload = env->prefork_gotwebd;
+	env->servers_pending = env->prefork_gotwebd;
+	env->gotweb_pending = env->prefork_gotwebd;
 
 	/* send our gotweb servers */
 	TAILQ_FOREACH(srv, &env->servers, entry) {
-		if (config_setserver(env, srv) == -1)
-			fatalx("%s: send server error", __func__);
+		if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_SRV,
+		    -1, srv, sizeof(*srv)) == -1)
+			fatal("main_compose_sockets GOTWEBD_IMSG_CFG_SRV");
+		if (main_compose_gotweb(env, GOTWEBD_IMSG_CFG_SRV,
+		    -1, srv, sizeof(*srv)) == -1)
+			fatal("main_compose_gotweb GOTWEBD_IMSG_CFG_SRV");
 	}
 
 	/* send our sockets */
 	TAILQ_FOREACH(sock, &env->sockets, entry) {
-		if (config_setsock(env, sock) == -1)
+		if (config_setsock(env, sock, uid, gid) == -1)
 			fatalx("%s: send socket error", __func__);
 	}
 
@@ -457,6 +602,9 @@ gotwebd_configure(struct gotwebd *env)
 	if (config_setfd(env) == -1)
 		fatalx("%s: send priv_fd error", __func__);
 
+	/* Connect servers and gotwebs. */
+	connect_children(env);
+
 	if (main_compose_sockets(env, GOTWEBD_IMSG_CFG_DONE, -1, NULL, 0) == -1)
 		fatal("main_compose_sockets GOTWEBD_IMSG_CFG_DONE");
 
@@ -466,15 +614,21 @@ gotwebd_configure(struct gotwebd *env)
 void
 gotwebd_configure_done(struct gotwebd *env)
 {
-	if (env->gotwebd_reload == 0) {
-		log_warnx("%s: configuration already finished", __func__);
-		return;
+	if (env->servers_pending > 0) {
+		env->servers_pending--;
+		if (env->servers_pending == 0 &&
+		    main_compose_sockets(env, GOTWEBD_IMSG_CTL_START,
+		        -1, NULL, 0) == -1)
+			fatal("main_compose_sockets GOTWEBD_IMSG_CTL_START");
 	}
 
-	env->gotwebd_reload--;
-	if (env->gotwebd_reload == 0 &&
-	    main_compose_sockets(env, GOTWEBD_CTL_START, -1, NULL, 0) == -1)
-		fatal("main_compose_sockets GOTWEBD_CTL_START");
+	if (env->gotweb_pending > 0) {
+		env->gotweb_pending--;
+		if (env->gotweb_pending == 0 &&
+		    main_compose_gotweb(env, GOTWEBD_IMSG_CTL_START,
+		        -1, NULL, 0) == -1)
+			fatal("main_compose_sockets GOTWEBD_IMSG_CTL_START");
+	}
 }
 
 void
@@ -492,6 +646,14 @@ gotwebd_shutdown(void)
 	}
 	free(env->iev_server);
 
+	for (i = 0; i < env->nserver; ++i) {
+		event_del(&env->iev_gotweb[i].ev);
+		imsgbuf_clear(&env->iev_gotweb[i].ibuf);
+		close(env->iev_gotweb[i].ibuf.fd);
+		env->iev_gotweb[i].ibuf.fd = -1;
+	}
+	free(env->iev_gotweb);
+
 	do {
 		pid = waitpid(WAIT_ANY, &status, 0);
 		if (pid <= 0)
blob - 2bbc2da98a42f54eda543a5d44e452364ab5402f
blob + df671fde047c618123e126aabe674ba02a5bccae
--- gotwebd/gotwebd.conf.5
+++ gotwebd/gotwebd.conf.5
@@ -44,15 +44,6 @@ For example:
 lan_addr = "192.168.0.1"
 listen on $lan_addr port 9090
 .Ed
-.Pp
-Paths mentioned in
-.Nm
-must be relative to
-.Pa /var/www ,
-the
-.Xr chroot 2
-environment of
-.Xr httpd 8 .
 .Sh GLOBAL CONFIGURATION
 The available global configuration directives are as follows:
 .Bl -tag -width Ds
@@ -64,6 +55,11 @@ environment of
 If not specified, it defaults to
 .Pa /var/www ,
 the home directory of the www user.
+Setting the
+.Ar path
+to
+.Pa /
+effectively disables chroot.
 .It Ic listen on Ar address Ic port Ar number
 Configure an address and port for incoming FastCGI connections.
 Valid
@@ -79,6 +75,11 @@ Configure a
 .Ux Ns -domain
 socket for incoming FastCGI connections.
 May be specified multiple times to build up a list of listening sockets.
+.Pp
+While the specified
+.Ar path
+must be absolute, it should usually point inside the web server's chroot
+directory such that the web server can access the socket.
 .It Ic prefork Ar number
 Run the specified number of server processes.
 .Xr gotwebd 8
@@ -88,7 +89,7 @@ Set the
 .Ar user
 which will run
 .Xr gotwebd 8 .
-If not specified, the user www will be used.
+If not specified, the user _gotwebd will be used.
 .El
 .Pp
 If no
@@ -123,10 +124,16 @@ Set the path to a custom Cascading Style Sheet (CSS) t
 If this option is not specified then the default style sheet
 .Sq gotweb.css
 will be used.
+.Pp
+This path must be valid in the web server's URL space since browsers
+will attempt to fetch it.
 .It Ic logo Ar path
 Set the path to an image file containing a logo to be displayed.
 Defaults to
 .Sq got.png .
+.Pp
+This path must be valid in the web server's URL space since browsers
+will attempt to fetch it.
 .It Ic logo_url Ar url
 Set a hyperlink for the logo.
 Defaults to
@@ -141,9 +148,17 @@ Set to zero to show all the repositories without pagin
 .It Ic repos_path Ar path
 Set the path to the directory which contains Git repositories that
 the server should publish.
+This path is absolute.
+Repositories can be served even if they reside outside the web server's
+chroot directory.
+.Pp
 Defaults to
 .Pa /got/public
-under the chroot.
+inside the web server's chroot directory.
+The
+.Cm chroot
+directive must be used before the server declaration in order to
+take effect.
 .It Ic respect_exportok Ar on | off
 Set whether to display the repository only if it contains the magic
 .Pa git-daemon-export-ok
@@ -231,13 +246,14 @@ server "localhost" {
 Another example, this time listening on a local port instead of the
 implicit
 .Ux
-socket.
+socket, and serving repositories located outside the web server's chroot:
 .Bd -literal -offset indent
 listen on 127.0.0.1 port 9000
 listen on ::1 port 9000
 
 server "localhost" {
-	site_name "my public repos"
+	site_name	"my public repos"
+	repos_path	"/var/git"
 }
 .Ed
 .Sh SEE ALSO
blob - 7dd491f1a07f53e526bf9e5df13d9e0767649384
blob + 0610e995e02d69a510d070b3f7e4907b00c068e7
--- gotwebd/gotwebd.h
+++ gotwebd/gotwebd.h
@@ -32,7 +32,7 @@
 #define GOTWEBD_CONF		 "/etc/gotwebd.conf"
 
 #ifndef GOTWEBD_DEFAULT_USER
-#define GOTWEBD_DEFAULT_USER	 "www"
+#define GOTWEBD_DEFAULT_USER	 "_gotwebd"
 #endif
 
 #define GOTWEBD_MAXDESCRSZ	 1024
@@ -117,12 +117,21 @@ struct got_blob_object;
 struct got_tree_entry;
 struct got_reflist_head;
 
+enum gotwebd_proc_type {
+	GOTWEBD_PROC_PARENT,
+	GOTWEBD_PROC_SERVER,
+	GOTWEBD_PROC_GOTWEB,
+};
+
 enum imsg_type {
 	GOTWEBD_IMSG_CFG_SRV,
 	GOTWEBD_IMSG_CFG_SOCK,
 	GOTWEBD_IMSG_CFG_FD,
 	GOTWEBD_IMSG_CFG_DONE,
-	GOTWEBD_CTL_START,
+	GOTWEBD_IMSG_CTL_PIPE,
+	GOTWEBD_IMSG_CTL_START,
+	GOTWEBD_IMSG_REQ_PROCESS,
+	GOTWEBD_IMSG_REQ_DONE,
 };
 
 struct imsgev {
@@ -229,6 +238,7 @@ enum socket_priv_fds {
 
 struct template;
 struct request {
+	TAILQ_ENTRY(request)		entry;
 	struct socket			*sock;
 	struct server			*srv;
 	struct transport		*t;
@@ -239,12 +249,16 @@ struct request {
 	uint16_t			 id;
 	int				 fd;
 	int				 priv_fd[PRIV_FDS__MAX];
+	int				 resp_fd;
+	struct event			*resp_event;
+	int				 sock_id;
+	uint32_t			 request_id;
 
-	uint8_t				 buf[FCGI_RECORD_SIZE];
+	uint8_t				 *buf;
 	size_t				 buf_pos;
 	size_t				 buf_len;
 
-	uint8_t				 outbuf[GOTWEBD_CACHESIZE];
+	uint8_t				 *outbuf;
 
 	char				 querystring[MAX_QUERYSTRING];
 	char				 document_uri[MAX_DOCUMENT_URI];
@@ -253,6 +267,7 @@ struct request {
 
 	uint8_t				 request_started;
 };
+TAILQ_HEAD(requestlist, request);
 
 struct fcgi_begin_request_body {
 	uint16_t	role;
@@ -349,12 +364,12 @@ struct gotwebd {
 
 	struct imsgev	*iev_parent;
 	struct imsgev	*iev_server;
+	struct imsgev	*iev_gotweb;
 	size_t		 nserver;
 
-	struct passwd	*pw;
-
 	uint16_t	 prefork_gotwebd;
-	int		 gotwebd_reload;
+	int		 servers_pending;
+	int		 gotweb_pending;
 
 	int		 server_cnt;
 
@@ -430,18 +445,22 @@ typedef int (*got_render_blame_line_cb)(struct templat
 /* gotwebd.c */
 void	 imsg_event_add(struct imsgev *);
 int	 imsg_compose_event(struct imsgev *, uint16_t, uint32_t,
-	    pid_t, int, const void *, uint16_t);
+	    pid_t, int, const void *, size_t);
 int	 main_compose_sockets(struct gotwebd *, uint32_t, int,
 	    const void *, uint16_t);
 int	 sockets_compose_main(struct gotwebd *, uint32_t,
 	    const void *, uint16_t);
+int	 main_compose_gotweb(struct gotwebd *, uint32_t, int,
+	    const void *, uint16_t);
 
 /* sockets.c */
 void sockets(struct gotwebd *, int);
 void sockets_parse_sockets(struct gotwebd *);
 void sockets_socket_accept(int, short, void *);
-int sockets_privinit(struct gotwebd *, struct socket *);
+int sockets_privinit(struct gotwebd *, struct socket *, uid_t, gid_t);
 void sockets_purge(struct gotwebd *);
+void sockets_rlimit(int);
+void sockets_del_request(struct request *);
 
 /* gotweb.c */
 void gotweb_index_navs(struct request *, struct gotweb_url *, int *,
@@ -455,6 +474,7 @@ void gotweb_free_repo_commit(struct repo_commit *);
 void gotweb_free_repo_tag(struct repo_tag *);
 void gotweb_process_request(struct request *);
 void gotweb_free_transport(struct transport *);
+void gotweb(struct gotwebd *, int);
 
 /* pages.tmpl */
 int	gotweb_render_page(struct template *, int (*)(struct template *));
@@ -507,7 +527,7 @@ const struct got_error *got_output_file_blame(struct r
 /* config.c */
 int config_setserver(struct gotwebd *, struct server *);
 int config_getserver(struct gotwebd *, struct imsg *);
-int config_setsock(struct gotwebd *, struct socket *);
+int config_setsock(struct gotwebd *, struct socket *, uid_t, gid_t);
 int config_getsock(struct gotwebd *, struct imsg *);
 int config_setfd(struct gotwebd *);
 int config_getfd(struct gotwebd *, struct imsg *);
blob - 5fd34708bd3654bc05060446ff5d55557747cfd3 (mode 644)
blob + /dev/null
--- gotwebd/libexec/Makefile
+++ /dev/null
@@ -1,4 +0,0 @@
-SUBDIR = got-read-blob got-read-commit got-read-object got-read-tree \
-	got-read-tag got-read-pack got-read-gitconfig got-read-gotconfig
-
-.include <bsd.subdir.mk>
blob - 85bee26728643214f5d4570f003572ac1fc36d05 (mode 644)
blob + /dev/null
--- gotwebd/libexec/Makefile.inc
+++ /dev/null
@@ -1,11 +0,0 @@
-.include "../Makefile.inc"
-
-realinstall:
-	if [ ! -d ${DESTDIR}${CHROOT_DIR}${LIBEXECDIR}/. ]; then \
-		${INSTALL} -d -o root -g daemon -m 755 \
-		    ${DESTDIR}${CHROOT_DIR}${LIBEXECDIR}; \
-	fi
-	${INSTALL} ${INSTALL_COPY} -o root -g daemon -m 755 ${PROG} \
-	    ${DESTDIR}${CHROOT_DIR}${LIBEXECDIR}/${PROG}
-
-NOMAN = Yes
blob - 9daacd33712bbd6a9ded56d7a03ed143ce6642d3 (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-blob/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-blob
-SRCS=		got-read-blob.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-blob
-
-.include <bsd.prog.mk>
blob - f45137c3ce56dbaadeaf0a5f0ffffd3b4204668c (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-commit/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-commit
-SRCS=		got-read-commit.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-commit
-
-.include <bsd.prog.mk>
blob - 44760613f030f1f1f62c831825d7bb85b07c9a46 (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-gitconfig/Makefile
+++ /dev/null
@@ -1,16 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-gitconfig
-SRCS=		got-read-gitconfig.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c gitconfig.c pollfd.c \
-		object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-gitconfig
-
-.include <bsd.prog.mk>
blob - 26dec69ed1be0648baa16cabea96c54dc3731c54 (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-gotconfig/Makefile
+++ /dev/null
@@ -1,19 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-gotconfig
-SRCS=		got-read-gotconfig.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c parse.y pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
-	-I${.CURDIR}/../../../libexec/got-read-gotconfig
-YFLAGS =
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-CLEANFILES = parse.h
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-gotconfig
-
-.include <bsd.prog.mk>
blob - 85841212056443f99c873f1089e3fef315ab7d2a (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-object/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-object
-SRCS=		got-read-object.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-object
-
-.include <bsd.prog.mk>
blob - 6ae7cd0c813fcc65f4256be6d06040df2d2880ba (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-pack/Makefile
+++ /dev/null
@@ -1,16 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-pack
-SRCS=		got-read-pack.c delta.c error.c inflate.c object_cache.c \
-		object_idset.c object_parse.c opentemp.c pack.c path.c \
-		privsep.c hash.c delta_cache.c pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-pack
-
-.include <bsd.prog.mk>
blob - eb7b682a069470aa6b445adfc142e1d7bdf7c4a4 (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-tag/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-tag
-SRCS=		got-read-tag.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-tag
-
-.include <bsd.prog.mk>
blob - 497d2ff1cd06d53bcc607653bb658219cd8ea982 (mode 644)
blob + /dev/null
--- gotwebd/libexec/got-read-tree/Makefile
+++ /dev/null
@@ -1,15 +0,0 @@
-.include "../../../got-version.mk"
-.include "../Makefile.inc"
-
-PROG=		got-read-tree
-SRCS=		got-read-tree.c error.c inflate.c object_parse.c \
-		path.c privsep.c hash.c pollfd.c object_qid.c
-
-CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib
-LDADD = -lutil -lz
-DPADD = ${LIBZ} ${LIBUTIL}
-LDSTATIC = ${STATIC}
-
-.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-tree
-
-.include <bsd.prog.mk>
blob - ce616554e436816c2be0cd52555746f30ab943f9
blob + 5fc49003ef44e659fe3e1b31dc57eb30e8c6fe69
--- gotwebd/parse.y
+++ gotwebd/parse.y
@@ -197,10 +197,16 @@ main		: PREFORK NUMBER {
 			n = strlcpy(gotwebd->httpd_chroot, $2,
 			    sizeof(gotwebd->httpd_chroot));
 			if (n >= sizeof(gotwebd->httpd_chroot)) {
-				yyerror("%s: httpd_chroot truncated", __func__);
+				yyerror("chroot path too long: %s", $2);
 				free($2);
 				YYERROR;
 			}
+			if (gotwebd->httpd_chroot[0] != '/') {
+				yyerror("chroot path must be an absolute path: "
+				    "bad path %s", gotwebd->httpd_chroot);
+				free($2);
+				YYERROR;
+			}
 			free($2);
 		}
 		| LISTEN ON listen_addr PORT STRING {
@@ -832,7 +838,18 @@ parse_config(const char *filename, struct gotwebd *env
 
 	/* add the implicit listen on socket */
 	if (TAILQ_EMPTY(&gotwebd->addresses)) {
-		const char *path = D_HTTPD_CHROOT D_UNIX_SOCKET;
+		char path[_POSIX_PATH_MAX];
+
+		if (strlcpy(path, gotwebd->httpd_chroot, sizeof(path))
+		    >= sizeof(path)) {
+			yyerror("chroot path too long: %s",
+			    gotwebd->httpd_chroot);
+		}
+		if (strlcat(path, D_UNIX_SOCKET, sizeof(path))
+		    >= sizeof(path)) {
+			yyerror("chroot path too long: %s",
+			    gotwebd->httpd_chroot);
+		}
 		if (get_unix_addr(path) == -1)
 			yyerror("can't listen on %s", path);
 	}
@@ -858,10 +875,14 @@ conf_new_server(const char *name)
 	n = strlcpy(srv->name, name, sizeof(srv->name));
 	if (n >= sizeof(srv->name))
 		fatalx("%s: strlcpy", __func__);
-	n = strlcpy(srv->repos_path, D_GOTPATH,
+	n = strlcpy(srv->repos_path, gotwebd->httpd_chroot,
 	    sizeof(srv->repos_path));
 	if (n >= sizeof(srv->repos_path))
 		fatalx("%s: strlcpy", __func__);
+	n = strlcat(srv->repos_path, D_GOTPATH,
+	    sizeof(srv->repos_path));
+	if (n >= sizeof(srv->repos_path))
+		fatalx("%s: strlcat", __func__);
 	n = strlcpy(srv->site_name, D_SITENAME,
 	    sizeof(srv->site_name));
 	if (n >= sizeof(srv->site_name))
blob - 4ca6de208a73860f721177a094834ffc846d1aaf
blob + fe41e8a1588f58c48515f05270570fa6735580c8
--- gotwebd/sockets.c
+++ gotwebd/sockets.c
@@ -42,6 +42,7 @@
 #include <netdb.h>
 #include <poll.h>
 #include <pwd.h>
+#include <siphash.h>
 #include <stddef.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -49,11 +50,7 @@
 #include <unistd.h>
 #include <util.h>
 
-#include "got_error.h"
-#include "got_opentemp.h"
 #include "got_reference.h"
-#include "got_repository.h"
-#include "got_privsep.h"
 
 #include "gotwebd.h"
 #include "log.h"
@@ -62,18 +59,17 @@
 #define SOCKS_BACKLOG 5
 #define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))
 
-volatile int client_cnt;
+static volatile int client_cnt;
 
 static struct timeval	timeout = { TIMEOUT_DEFAULT, 0 };
 
 static void	 sockets_sighdlr(int, short, void *);
 static void	 sockets_shutdown(void);
-static void	 sockets_launch(void);
+static void	 sockets_launch(struct gotwebd *);
 static void	 sockets_accept_paused(int, short, void *);
-static void	 sockets_rlimit(int);
 
 static void	 sockets_dispatch_main(int, short, void *);
-static int	 sockets_unix_socket_listen(struct gotwebd *, struct socket *);
+static int	 sockets_unix_socket_listen(struct gotwebd *, struct socket *, uid_t, gid_t);
 static int	 sockets_create_socket(struct address *);
 static int	 sockets_accept_reserve(int, struct sockaddr *, socklen_t *,
 		    int, volatile int *);
@@ -83,12 +79,94 @@ 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++;
+}
+
 void
+sockets_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;
+}
+
+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);
@@ -118,8 +196,7 @@ sockets(struct gotwebd *env, int fd)
 	signal_add(&sigterm, NULL);
 
 #ifndef PROFILE
-	if (pledge("stdio rpath inet recvfd proc exec sendfd unveil",
-	    NULL) == -1)
+	if (pledge("stdio inet recvfd sendfd", NULL) == -1)
 		fatal("pledge");
 #endif
 
@@ -190,12 +267,13 @@ sockets_conf_new_socket(struct gotwebd *env, int id, s
 }
 
 static void
-sockets_launch(void)
+sockets_launch(struct gotwebd *env)
 {
 	struct socket *sock;
-	struct server *srv;
-	const struct got_error *error;
 
+	if (env->iev_gotweb == NULL)
+		fatal("gotweb process not connected");
+
 	TAILQ_FOREACH(sock, &gotwebd_env->sockets, entry) {
 		log_info("%s: configuring socket %d (%d)", __func__,
 		    sock->conf.id, sock->fd);
@@ -212,17 +290,12 @@ sockets_launch(void)
 		    sock->conf.id);
 	}
 
-	TAILQ_FOREACH(srv, &gotwebd_env->servers, entry) {
-		if (unveil(srv->repos_path, "r") == -1)
-			fatal("unveil %s", srv->repos_path);
-	}
+#ifndef PROFILE
+	if (pledge("stdio inet sendfd", NULL) == -1)
+		fatal("pledge");
+#endif
+	event_add(&env->iev_gotweb->ev, NULL);
 
-	error = got_privsep_unveil_exec_helpers();
-	if (error)
-		fatal("%s", error->msg);
-
-	if (unveil(NULL, NULL) == -1)
-		fatal("unveil");
 }
 
 void
@@ -246,6 +319,110 @@ sockets_purge(struct gotwebd *env)
 }
 
 static void
+request_done(struct imsg *imsg)
+{
+	struct request *c;
+	uint32_t request_id;
+	size_t datalen = imsg->hdr.len - IMSG_HEADER_SIZE;
+
+	if (datalen != sizeof(request_id)) {
+		log_warn("IMSG_REQ_DONE with bad data length");
+		return;
+	}
+
+	memcpy(&request_id, imsg->data, sizeof(request_id));
+
+	c = find_request(request_id);
+	if (c == NULL) {
+		log_warnx("no request to clean up found for ID '%d'",
+		    request_id);
+		return;
+	}
+
+	fcgi_create_end_record(c);
+	fcgi_cleanup_request(c);
+}
+
+static void
+server_dispatch_gotweb(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_REQ_DONE:
+			request_done(&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_gotweb_pipe(struct gotwebd *env, struct imsg *imsg)
+{
+	struct imsgev *iev;
+	int fd;
+
+	if (env->iev_gotweb != NULL) {
+		log_warn("gotweb 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);
+
+	iev->handler = server_dispatch_gotweb;
+	iev->data = iev;
+	event_set(&iev->ev, fd, EV_READ, server_dispatch_gotweb, iev);
+
+	env->iev_gotweb = iev;
+}
+
+static void
 sockets_dispatch_main(int fd, short event, void *arg)
 {
 	struct imsgev		*iev = arg;
@@ -281,15 +458,15 @@ sockets_dispatch_main(int fd, short event, void *arg)
 		case GOTWEBD_IMSG_CFG_SOCK:
 			config_getsock(env, &imsg);
 			break;
-		case GOTWEBD_IMSG_CFG_FD:
-			config_getfd(env, &imsg);
-			break;
 		case GOTWEBD_IMSG_CFG_DONE:
 			config_getcfg(env, &imsg);
 			break;
-		case GOTWEBD_CTL_START:
-			sockets_launch();
+		case GOTWEBD_IMSG_CTL_PIPE:
+			recv_gotweb_pipe(env, &imsg);
 			break;
+		case GOTWEBD_IMSG_CTL_START:
+			sockets_launch(env);
+			break;
 		default:
 			fatalx("%s: unknown imsg type %d", __func__,
 			    imsg.hdr.type);
@@ -354,6 +531,8 @@ sockets_shutdown(void)
 		free(h);
 	}
 
+	requests_purge();
+
 	imsgbuf_clear(&gotwebd_env->iev_parent->ibuf);
 	free(gotwebd_env->iev_parent);
 	free(gotwebd_env);
@@ -362,12 +541,12 @@ sockets_shutdown(void)
 }
 
 int
-sockets_privinit(struct gotwebd *env, struct socket *sock)
+sockets_privinit(struct gotwebd *env, struct socket *sock, uid_t uid, gid_t gid)
 {
 	if (sock->conf.af_type == AF_UNIX) {
 		log_info("%s: initializing unix socket %s", __func__,
 		    sock->conf.unix_socket_name);
-		sock->fd = sockets_unix_socket_listen(env, sock);
+		sock->fd = sockets_unix_socket_listen(env, sock, uid, gid);
 		if (sock->fd == -1)
 			return -1;
 	}
@@ -385,7 +564,8 @@ sockets_privinit(struct gotwebd *env, struct socket *s
 }
 
 static int
-sockets_unix_socket_listen(struct gotwebd *env, struct socket *sock)
+sockets_unix_socket_listen(struct gotwebd *env, struct socket *sock,
+    uid_t uid, gid_t gid)
 {
 	int u_fd = -1;
 	mode_t old_umask, mode;
@@ -425,8 +605,7 @@ sockets_unix_socket_listen(struct gotwebd *env, struct
 		return -1;
 	}
 
-	if (chown(sock->conf.unix_socket_name, env->pw->pw_uid,
-	    env->pw->pw_gid) == -1) {
+	if (chown(sock->conf.unix_socket_name, uid, gid) == -1) {
 		log_warn("%s: chown", __func__);
 		close(u_fd);
 		(void)unlink(sock->conf.unix_socket_name);
@@ -521,6 +700,7 @@ sockets_socket_accept(int fd, short event, void *arg)
 	struct sockaddr_storage ss;
 	struct timeval backoff;
 	struct request *c = NULL;
+	uint8_t *buf = NULL;
 	socklen_t len;
 	int s;
 
@@ -557,28 +737,32 @@ sockets_socket_accept(int fd, short event, void *arg)
 
 	c = calloc(1, sizeof(struct request));
 	if (c == NULL) {
-		log_warn("%s", __func__);
+		log_warn("%s: calloc", __func__);
 		close(s);
 		cgi_inflight--;
 		return;
 	}
 
-	c->tp = template(c, &fcgi_write, c->outbuf, sizeof(c->outbuf));
-	if (c->tp == NULL) {
-		log_warn("%s", __func__);
+	buf = calloc(1, FCGI_RECORD_SIZE);
+	if (buf == NULL) {
+		log_warn("%s: calloc", __func__);
 		close(s);
 		cgi_inflight--;
 		free(c);
 		return;
 	}
 
+	c->buf = buf;
 	c->fd = s;
+	c->resp_fd = -1;
 	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->sock->client_status = CLIENT_CONNECT;
+	c->request_id = get_request_id();
 
 	event_set(&c->ev, s, EV_READ|EV_PERSIST, fcgi_request, c);
 	event_add(&c->ev, NULL);
@@ -586,8 +770,7 @@ sockets_socket_accept(int fd, short event, void *arg)
 	evtimer_set(&c->tmo, fcgi_timeout, c);
 	evtimer_add(&c->tmo, &timeout);
 
-	client_cnt++;
-
+	add_request(c);
 	return;
 err:
 	cgi_inflight--;
@@ -596,7 +779,7 @@ err:
 		free(c);
 }
 
-static void
+void
 sockets_rlimit(int maxfd)
 {
 	struct rlimit rl;
blob - 803eb4958024659ae980b5f51216764b4ebbe37b
blob + 9cb243fe1e217fa2ced376a1954ea7ba7faa879b
--- regress/gotwebd/Makefile
+++ regress/gotwebd/Makefile
@@ -59,14 +59,8 @@ ensure_root:
 		false; \
 	fi
 
-gotwebd_libexec:
-	@su -m ${GOTWEBD_TEST_USER} -c \
-	    '${MAKE} -C ${.CURDIR}/../../gotwebd/libexec' >/dev/null 2>&1
-
-prepare_test_env: ensure_root gotwebd_libexec
+prepare_test_env: ensure_root
 	@mkdir -p "${GOTWEBD_TEST_CHROOT}"
-	@DESTDIR=${GOTWEBD_TEST_ROOT} \
-	    ${MAKE} -C ${.CURDIR}/../../gotwebd/libexec install >/dev/null 2>&1
 	@chown ${GOTWEBD_TEST_USER} "${GOTWEBD_TEST_ROOT}" \
 	    "${GOTWEBD_TEST_CHROOT}"