Commit Diff


commit - 84bc347c349f09dc692e8d760ba9af3d21b0f5af
commit + a596b9579655b53f1549498aff25ae1d2c9dfc81
blob - 8a7d489ac1ca0b7fd0cf744dfdea4257efcda1c9
blob + 8b6c6977676250da107793984432e332c86e1251
--- Makefile
+++ Makefile
@@ -7,7 +7,7 @@ SUBDIR += regress
 .endif
 
 .if make(clean) || make(obj) || make(release)
-SUBDIR += gotweb
+SUBDIR += gotweb gotwebd
 .endif
 
 .if make(tags) || make(cleandir)
@@ -39,4 +39,10 @@ web:
 web-install:
 	${MAKE} -C gotweb install
 
+webd:
+	${MAKE} -C gotwebd
+
+webd-install:
+	${MAKE} -C gotwebd install
+
 .include <bsd.subdir.mk>
blob - /dev/null
blob + 4c0096efb52bb79e4a8f7e5f09c1b1d953009636 (mode 644)
--- /dev/null
+++ gotwebd/Makefile
@@ -0,0 +1,55 @@
+.PATH:${.CURDIR}/../lib
+
+SUBDIR = libexec
+
+.include "../got-version.mk"
+.include "Makefile.inc"
+
+PROG =		gotwebd
+SRCS =		config.c sockets.c log.c gotwebd.c parse.y proc.c \
+		fcgi.c gotweb.c got_operations.c
+SRCS +=		blame.c commit_graph.c delta.c diff.c \
+		diffreg.c error.c fileindex.c object.c object_cache.c \
+		object_idset.c object_parse.c opentemp.c path.c pack.c \
+		privsep.c reference.c repository.c sha1.c worktree.c \
+		utf8.c inflate.c buf.c rcsutil.c diff3.c \
+		lockfile.c deflate.c object_create.c delta_cache.c \
+		gotconfig.c diff_main.c diff_atomize_text.c diff_myers.c \
+		diff_output.c diff_output_plain.c diff_output_unidiff.c \
+		diff_output_edscript.c diff_patience.c bloom.c murmurhash2.c \
+		worktree_open.c patch.c sigs.c date.c
+
+MAN =		${PROG}.conf.5 ${PROG}.8
+
+CPPFLAGS +=	-I${.CURDIR}/../include -I${.CURDIR}/../lib -I${.CURDIR}
+LDADD +=	-lz -levent -lutil -lm
+YFLAGS =
+DPADD =		${LIBEVENT} ${LIBUTIL}
+#CFLAGS +=	-DGOT_NO_OBJ_CACHE
+
+.if ${GOT_RELEASE} != "Yes"
+NOMAN = Yes
+.endif
+
+.if defined(PROFILE)
+CPPFLAGS += -DPROFILE
+DEBUG = -O0 -pg -g -static
+.else
+DEBUG = -O0 -g
+.endif
+
+realinstall:
+	if [ ! -d ${DESTDIR}${PUB_REPOS_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${PUB_REPOS_DIR}; \
+	fi
+	${INSTALL} -c -o root -g daemon -m 0755 ${PROG} ${BINDIR}/${PROG}
+	if [ ! -d ${DESTDIR}${HTTPD_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${HTTPD_DIR}; \
+	fi
+	if [ ! -d ${DESTDIR}${PROG_DIR}/. ]; then \
+		${INSTALL} -d -o root -g daemon -m 755 ${DESTDIR}${PROG_DIR}; \
+	fi
+	${INSTALL} -c -o ${WWWUSR} -g ${WWWGRP} -m 0755 \
+	    ${.CURDIR}/files/htdocs/${PROG}/* ${DESTDIR}${PROG_DIR}
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 8e5361495585907a0f12661cad8b8b9fbfb0b503 (mode 644)
--- /dev/null
+++ gotwebd/Makefile.inc
@@ -0,0 +1,11 @@
+LDADD +=	-lz -lutil
+PREFIX ?=	/usr/local
+BINDIR ?=	${PREFIX}/sbin
+CHROOT_DIR ?=	/var/www
+GOTWEB_DIR =	/bin/gotwebd
+LIBEXECDIR =	${GOTWEB_DIR}/libexec
+LIBEXEC_DIR =	${CHROOT_DIR}${LIBEXECDIR}
+HTTPD_DIR =	${CHROOT_DIR}/htdocs
+PROG_DIR =	${HTTPD_DIR}/${PROG}
+WWWUSR ?=	www
+WWWGRP ?=	www
blob - /dev/null
blob + 4a8874a87e47d8824f253f466514c16f8ce8c881 (mode 644)
--- /dev/null
+++ gotwebd/config.c
@@ -0,0 +1,325 @@
+/*
+ * Copyright (c) 2020-2021 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/time.h>
+#include <sys/uio.h>
+#include <sys/socket.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <termios.h>
+#include <unistd.h>
+#include <limits.h>
+#include <string.h>
+#include <event.h>
+#include <fcntl.h>
+#include <util.h>
+#include <errno.h>
+#include <imsg.h>
+
+#include "got_opentemp.h"
+
+#include "proc.h"
+#include "gotwebd.h"
+
+int
+config_init(struct gotwebd *env)
+{
+	struct privsep *ps = env->gotwebd_ps;
+	unsigned int what;
+
+	/* Global configuration. */
+	if (privsep_process == PROC_GOTWEBD)
+		env->prefork_gotwebd = GOTWEBD_NUMPROC;
+
+	ps->ps_what[PROC_GOTWEBD] = CONFIG_ALL;
+	ps->ps_what[PROC_SOCKS] = CONFIG_SOCKS;
+
+	/* Other configuration. */
+	what = ps->ps_what[privsep_process];
+	if (what & CONFIG_SOCKS) {
+		env->server_cnt = 0;
+		env->servers = calloc(1, sizeof(*env->servers));
+		if (env->servers == NULL)
+			fatalx("%s: calloc", __func__);
+		env->sockets = calloc(1, sizeof(*env->sockets));
+		if (env->sockets == NULL)
+			fatalx("%s: calloc", __func__);
+		TAILQ_INIT(env->servers);
+		TAILQ_INIT(env->sockets);
+	}
+	 return 0;
+}
+
+int
+config_getcfg(struct gotwebd *env, struct imsg *imsg)
+{
+	/* nothing to do but tell gotwebd configuration is done */
+	if (privsep_process != PROC_GOTWEBD)
+		proc_compose(env->gotwebd_ps, PROC_GOTWEBD,
+		    IMSG_CFG_DONE, NULL, 0);
+
+	 return 0;
+}
+
+int
+config_setserver(struct gotwebd *env, struct server *srv)
+{
+	struct server ssrv;
+	struct privsep *ps = env->gotwebd_ps;
+
+	memcpy(&ssrv, srv, sizeof(ssrv));
+	proc_compose(ps, PROC_SOCKS, IMSG_CFG_SRV, &ssrv, sizeof(ssrv));
+	 return 0;
+}
+
+int
+config_getserver(struct gotwebd *env, struct imsg *imsg)
+{
+	struct server *srv;
+	uint8_t *p = imsg->data;
+
+	IMSG_SIZE_CHECK(imsg, &srv);
+
+	srv = calloc(1, sizeof(*srv));
+	if (srv == NULL)
+		fatalx("%s: calloc", __func__);
+	memcpy(srv, p, sizeof(*srv));
+
+	if (IMSG_DATA_SIZE(imsg) != sizeof(*srv)) {
+		log_debug("%s: imsg size error", __func__);
+		free(srv);
+		return 1;
+	}
+
+	/* log server info */
+	log_debug("%s: server=%s fcgi_socket=%s unix_socket=%s", __func__,
+	    srv->name, srv->fcgi_socket ? "yes" : "no", srv->unix_socket ?
+	    "yes" : "no");
+
+	TAILQ_INSERT_TAIL(env->servers, srv, entry);
+
+	 return 0;
+}
+
+int
+config_setsock(struct gotwebd *env, struct socket *sock)
+{
+	struct privsep *ps = env->gotwebd_ps;
+	struct socket_conf s;
+	int id;
+	int fd = -1, n, m;
+	struct iovec iov[6];
+	size_t c;
+	unsigned int what;
+
+	/* open listening sockets */
+	if (sockets_privinit(env, sock) == -1)
+		return -1;
+
+	for (id = 0; id < PROC_MAX; id++) {
+		what = ps->ps_what[id];
+
+		if ((what & CONFIG_SOCKS) == 0 || id == privsep_process)
+			continue;
+
+		memcpy(&s, &sock->conf, sizeof(s));
+
+		c = 0;
+		iov[c].iov_base = &s;
+		iov[c++].iov_len = sizeof(s);
+
+		if (id == PROC_SOCKS) {
+			/* XXX imsg code will close the fd after 1st call */
+			n = -1;
+			proc_range(ps, id, &n, &m);
+			for (n = 0; n < m; n++) {
+				if (sock->fd == -1)
+					fd = -1;
+				else if ((fd = dup(sock->fd)) == -1)
+					return 1;
+				if (proc_composev_imsg(ps, id, n, IMSG_CFG_SOCK,
+				    -1, fd, iov, c) != 0) {
+					log_warn("%s: failed to compose "
+					    "IMSG_CFG_SOCK imsg",
+					    __func__);
+					return 1;
+				}
+				if (proc_flush_imsg(ps, id, n) == -1) {
+					log_warn("%s: failed to flush "
+					    "IMSG_CFG_SOCK imsg",
+					    __func__);
+					return 1;
+				}
+			}
+		}
+	}
+
+	/* Close socket early to prevent fd exhaustion in gotwebd. */
+	if (sock->fd != -1) {
+		close(sock->fd);
+		sock->fd = -1;
+	}
+
+	 return 0;
+}
+
+int
+config_getsock(struct gotwebd *env, struct imsg *imsg)
+{
+	struct socket *sock = NULL;
+	struct socket_conf sock_conf;
+	uint8_t *p = imsg->data;
+	int i;
+
+	IMSG_SIZE_CHECK(imsg, &sock_conf);
+	memcpy(&sock_conf, p, sizeof(sock_conf));
+
+	if (IMSG_DATA_SIZE(imsg) != sizeof(sock_conf)) {
+		log_debug("%s: imsg size error", __func__);
+		return 1;
+	}
+
+	/* create a new socket */
+	if ((sock = calloc(1, sizeof(*sock))) == NULL) {
+		if (imsg->fd != -1)
+			close(imsg->fd);
+		return 1;
+	}
+
+	memcpy(&sock->conf, &sock_conf, sizeof(sock->conf));
+	sock->fd = imsg->fd;
+
+	TAILQ_INSERT_TAIL(env->sockets, sock, entry);
+
+	for (i = 0; i < PRIV_FDS__MAX; i++)
+		sock->priv_fd[i] = -1;
+
+	for (i = 0; i < GOT_PACK_NUM_TEMPFILES; i++)
+		sock->pack_fds[i] = -1;
+
+	/* log new socket info */
+	log_debug("%s: name=%s id=%d server=%s child_id=%d parent_id=%d "
+	    "type=%s ipv4=%d ipv6=%d socket_path=%s",
+	    __func__, sock->conf.name, sock->conf.id, sock->conf.srv_name,
+	    sock->conf.child_id, sock->conf.parent_id, sock->conf.type ?
+	    "fcgi" : "unix", sock->conf.ipv4, sock->conf.ipv6,
+	    strlen(sock->conf.unix_socket_name) ?
+	    sock->conf.unix_socket_name : "none");
+
+	 return 0;
+}
+
+int
+config_setfd(struct gotwebd *env, struct socket *sock)
+{
+	struct privsep *ps = env->gotwebd_ps;
+	int id, s;
+	int fd = -1, n, m, j;
+	struct iovec iov[6];
+	size_t c;
+	unsigned int what;
+
+	log_debug("%s: Allocating %d file descriptors",
+	    __func__, PRIV_FDS__MAX + GOT_PACK_NUM_TEMPFILES);
+
+	for (j = 0; j < PRIV_FDS__MAX + GOT_PACK_NUM_TEMPFILES; j++) {
+		for (id = 0; id < PROC_MAX; id++) {
+			what = ps->ps_what[id];
+
+			if ((what & CONFIG_SOCKS) == 0 || id == privsep_process)
+				continue;
+
+			s = sock->conf.id;
+			c = 0;
+			iov[c].iov_base = &s;
+			iov[c++].iov_len = sizeof(s);
+
+			if (id == PROC_SOCKS) {
+				/*
+				 * XXX imsg code will close the fd
+				 * after 1st call
+				 */
+				n = -1;
+				proc_range(ps, id, &n, &m);
+				for (n = 0; n < m; n++) {
+					fd = got_opentempfd();
+					if (fd == -1)
+						return 1;
+					if (proc_composev_imsg(ps, id, n,
+					    IMSG_CFG_FD, -1, fd, iov, c) != 0) {
+						log_warn("%s: failed to compose "
+						    "IMSG_CFG_FD imsg",
+						    __func__);
+						return 1;
+					}
+					if (proc_flush_imsg(ps, id, n) == -1) {
+						log_warn("%s: failed to flush "
+						    "IMSG_CFG_FD imsg",
+						    __func__);
+						return 1;
+					}
+				}
+			}
+		}
+
+		/* Close fd early to prevent fd exhaustion in gotwebd. */
+		if (fd != -1)
+			close(fd);
+	}
+	return 0;
+}
+
+int
+config_getfd(struct gotwebd *env, struct imsg *imsg)
+{
+	struct socket *sock;
+	uint8_t *p = imsg->data;
+	int sock_id, match = 0, i;
+
+	IMSG_SIZE_CHECK(imsg, &sock_id);
+	memcpy(&sock_id, p, sizeof(sock_id));
+
+	TAILQ_FOREACH(sock, env->sockets, entry) {
+		for (i = 0; i < (GOT_PACK_NUM_TEMPFILES + PRIV_FDS__MAX); i++) {
+			if (i < PRIV_FDS__MAX && sock->priv_fd[i] == -1) {
+				log_debug("%s: assigning socket %d priv_fd %d",
+				    __func__, sock_id, imsg->fd);
+				sock->priv_fd[i] = imsg->fd;
+				match = 1;
+				break;
+			}
+			if (sock->pack_fds[i - PRIV_FDS__MAX] == -1) {
+				log_debug("%s: assigning socket %d pack_fd %d",
+				    __func__, sock_id, imsg->fd);
+				sock->pack_fds[i - PRIV_FDS__MAX] = imsg->fd;
+				match = 1;
+				break;
+			}
+		}
+	}
+
+	if (match)
+		 return 0;
+	else
+		return 1;
+}
blob - /dev/null
blob + 1581cf938674fc59b27476119bfcc54b5b4417b5 (mode 644)
--- /dev/null
+++ gotwebd/fcgi.c
@@ -0,0 +1,511 @@
+/*
+ * Copyright (c) 2020-2022 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
+ * Copyright (c) 2013 Florian Obser <florian@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <arpa/inet.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <errno.h>
+#include <event.h>
+#include <imsg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "got_error.h"
+
+#include "proc.h"
+#include "gotwebd.h"
+
+size_t	 fcgi_parse_record(uint8_t *, size_t, struct request *);
+void	 fcgi_parse_begin_request(uint8_t *, uint16_t, struct request *,
+	    uint16_t);
+void	 fcgi_parse_params(uint8_t *, uint16_t, struct request *, uint16_t);
+void	 fcgi_send_response(struct request *, struct fcgi_response *);
+
+void	 dump_fcgi_record_header(const char *, struct fcgi_record_header *);
+void	 dump_fcgi_begin_request_body(const char *,
+	    struct fcgi_begin_request_body *);
+void	 dump_fcgi_end_request_body(const char *,
+	    struct fcgi_end_request_body *);
+
+extern int	 cgi_inflight;
+extern volatile int client_cnt;
+
+void
+fcgi_request(int fd, short events, void *arg)
+{
+	struct request *c = arg;
+	ssize_t n;
+	size_t parsed = 0;
+
+	n = read(fd, c->buf + c->buf_pos + c->buf_len,
+	    FCGI_RECORD_SIZE - c->buf_pos-c->buf_len);
+
+	switch (n) {
+	case -1:
+		switch (errno) {
+		case EINTR:
+		case EAGAIN:
+			return;
+		default:
+			goto fail;
+		}
+		break;
+
+	case 0:
+		log_debug("closed connection");
+		goto fail;
+	default:
+		break;
+	}
+
+	c->buf_len += n;
+
+	/*
+	 * 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 (parsed != 0) {
+			c->buf_pos += parsed;
+			c->buf_len -= parsed;
+		}
+	} while (parsed > 0 && c->buf_len > 0);
+
+	/* Make space for further reads */
+	if (parsed != 0)
+		if (c->buf_len > 0) {
+			bcopy(c->buf + c->buf_pos, c->buf, c->buf_len);
+			c->buf_pos = 0;
+		}
+	return;
+fail:
+	fcgi_cleanup_request(c);
+}
+
+size_t
+fcgi_parse_record(uint8_t *buf, size_t n, struct request *c)
+{
+	struct fcgi_record_header *h;
+
+	if (n < sizeof(struct fcgi_record_header))
+		 return 0;
+
+	h = (struct fcgi_record_header*) buf;
+
+	dump_fcgi_record("", h);
+
+	if (n < sizeof(struct fcgi_record_header) + ntohs(h->content_len)
+	    + h->padding_len)
+		 return 0;
+
+	if (h->version != 1)
+		log_warn("wrong version");
+
+	switch (h->type) {
+	case FCGI_BEGIN_REQUEST:
+		fcgi_parse_begin_request(buf +
+		    sizeof(struct fcgi_record_header),
+		    ntohs(h->content_len), c, ntohs(h->id));
+		break;
+	case FCGI_PARAMS:
+		fcgi_parse_params(buf + sizeof(struct fcgi_record_header),
+		    ntohs(h->content_len), c, ntohs(h->id));
+		break;
+	case FCGI_STDIN:
+	case FCGI_ABORT_REQUEST:
+		fcgi_create_end_record(c);
+		fcgi_cleanup_request(c);
+		return 0;
+	default:
+		log_warn("unimplemented type %d", h->type);
+		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)
+{
+	/* 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;
+	SLIST_INIT(&c->env);
+	c->env_count = 0;
+}
+
+void
+fcgi_parse_params(uint8_t *buf, uint16_t n, struct request *c, uint16_t id)
+{
+	struct env_val *env_entry;
+	uint32_t name_len, val_len;
+	uint8_t *sd, *dr_buf;
+
+	if (!c->request_started) {
+		log_warn("FCGI_PARAMS without FCGI_BEGIN_REQUEST, ignoring");
+		return;
+	}
+
+	if (c->id != id) {
+		log_warn("unexpected id, ignoring");
+		return;
+	}
+
+	if (n == 0) {
+		gotweb_process_request(c);
+		return;
+	}
+
+	while (n > 0) {
+		if (buf[0] >> 7 == 0) {
+			name_len = buf[0];
+			n--;
+			buf++;
+		} else {
+			if (n > 3) {
+				name_len = ((buf[0] & 0x7f) << 24) +
+				    (buf[1] << 16) + (buf[2] << 8) + buf[3];
+				n -= 4;
+				buf += 4;
+			} else
+				return;
+		}
+
+		if (n > 0) {
+			if (buf[0] >> 7 == 0) {
+				val_len = buf[0];
+				n--;
+				buf++;
+			} else {
+				if (n > 3) {
+					val_len = ((buf[0] & 0x7f) << 24) +
+					    (buf[1] << 16) + (buf[2] << 8) +
+					     buf[3];
+					n -= 4;
+					buf += 4;
+				} else
+					return;
+			}
+		} else
+			return;
+
+		if (n < name_len + val_len)
+			return;
+
+		if ((env_entry = malloc(sizeof(struct env_val))) == NULL) {
+			log_warn("cannot malloc env_entry");
+			return;
+		}
+
+		if ((env_entry->val = calloc(sizeof(char), name_len + val_len +
+		    2)) == NULL) {
+			log_warn("cannot allocate env_entry->val");
+			free(env_entry);
+			return;
+		}
+
+		bcopy(buf, env_entry->val, name_len);
+		buf += name_len;
+		n -= name_len;
+
+		env_entry->val[name_len] = '\0';
+		if (val_len < MAX_QUERYSTRING && strcmp(env_entry->val,
+		    "QUERY_STRING") == 0 && c->querystring[0] == '\0') {
+			bcopy(buf, c->querystring, val_len);
+			c->querystring[val_len] = '\0';
+		}
+		if (val_len < GOTWEBD_MAXTEXT && strcmp(env_entry->val,
+		    "HTTP_HOST") == 0 && c->http_host[0] == '\0') {
+
+			/*
+			 * lazily get subdomain
+			 * will only get domain if no subdomain exists
+			 * this can still work if gotweb server name is the same
+			 */
+			sd = strchr(buf, '.');
+			if (sd)
+				*sd = '\0';
+
+			bcopy(buf, c->http_host, val_len);
+			c->http_host[val_len] = '\0';
+		}
+		if (val_len < MAX_DOCUMENT_ROOT && strcmp(env_entry->val,
+		    "DOCUMENT_ROOT") == 0 && c->document_root[0] == '\0') {
+
+			/* drop first char, as it's always / */
+			dr_buf = &buf[1];
+
+			bcopy(dr_buf, c->document_root, val_len - 1);
+			c->document_root[val_len] = '\0';
+		}
+		if (val_len < MAX_SERVER_NAME && strcmp(env_entry->val,
+		    "SERVER_NAME") == 0 && c->server_name[0] == '\0') {
+			/* drop first char, as it's always / */
+
+			bcopy(buf, c->server_name, val_len);
+			c->server_name[val_len] = '\0';
+		}
+		env_entry->val[name_len] = '=';
+
+		bcopy(buf, (env_entry->val) + name_len + 1, val_len);
+		buf += val_len;
+		n -= val_len;
+
+		SLIST_INSERT_HEAD(&c->env, env_entry, entry);
+		log_debug("env[%d], %s", c->env_count, env_entry->val);
+		c->env_count++;
+	}
+}
+
+void
+fcgi_timeout(int fd, short events, void *arg)
+{
+	fcgi_cleanup_request((struct request*) arg);
+}
+
+int
+fcgi_gen_binary_response(struct request *c, const uint8_t *data, int len)
+{
+	struct fcgi_response *resp;
+	struct fcgi_record_header *header;
+	ssize_t n = 0;
+	int i;
+
+	if (c->sock->client_status == CLIENT_DISCONNECT)
+		return -1;
+
+	if (data == NULL)
+		return 0;
+
+	if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) {
+		log_warn("%s: cannot calloc fcgi_response", __func__);
+		return -1;
+	}
+
+	header = (struct fcgi_record_header*) resp->data;
+	header->version = 1;
+	header->type = FCGI_STDOUT;
+	header->id = htons(c->id);
+	header->padding_len = 0;
+	header->reserved = 0;
+
+	for (i = 0; i < len; i++) {
+		resp->data[i+8] = data[i];
+		n++;
+	}
+
+	header->content_len = htons(n);
+	resp->data_pos = 0;
+	resp->data_len = n + sizeof(struct fcgi_record_header);
+	fcgi_send_response(c, resp);
+
+	return 0;
+}
+
+int
+fcgi_gen_response(struct request *c, const char *data)
+{
+	struct fcgi_response *resp;
+	struct fcgi_record_header *header;
+	ssize_t n = 0;
+	int i;
+
+	if (c->sock->client_status == CLIENT_DISCONNECT)
+		return -1;
+
+	if (data == NULL)
+		return 0;
+
+	if (strlen(data) == 0)
+		return 0;
+
+	if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) {
+		log_warn("%s: cannot calloc fcgi_response", __func__);
+		return -1;
+	}
+
+	header = (struct fcgi_record_header*) resp->data;
+	header->version = 1;
+	header->type = FCGI_STDOUT;
+	header->id = htons(c->id);
+	header->padding_len = 0;
+	header->reserved = 0;
+
+	for (i = 0; i < strlen(data); i++) {
+		resp->data[i+8] = data[i];
+		n++;
+	}
+
+	header->content_len = htons(n);
+	resp->data_pos = 0;
+	resp->data_len = n + sizeof(struct fcgi_record_header);
+	fcgi_send_response(c, resp);
+
+	return 0;
+}
+
+void
+fcgi_send_response(struct request *c, struct fcgi_response *resp)
+{
+	struct fcgi_record_header *header;
+	struct timespec ts;
+	size_t padded_len;
+	int err = 0, th = 2000;
+
+	ts.tv_sec = 0;
+	ts.tv_nsec = 50;
+
+	header = (struct fcgi_record_header*)resp->data;
+
+	/* The FastCGI spec suggests to align the output buffer */
+	padded_len = FCGI_ALIGN(resp->data_len);
+	if (padded_len > resp->data_len) {
+		/* There should always be FCGI_PADDING_SIZE bytes left */
+		if (padded_len > FCGI_RECORD_SIZE)
+			log_warn("response too long");
+		header->padding_len = padded_len - resp->data_len;
+		resp->data_len = padded_len;
+	}
+
+	dump_fcgi_record("resp ", header);
+
+	/*
+	 * XXX: add some simple write heuristics here
+	 * On slower VMs, spotty connections, etc., we don't want to go right to
+	 * disconnect. Let's at least try to write the data a few times before
+	 * giving up.
+	 */
+	while ((write(c->fd, resp->data + resp->data_pos,
+	    resp->data_len)) == -1) {
+		nanosleep(&ts, NULL);
+		err++;
+		if (err == th) {
+			c->sock->client_status = CLIENT_DISCONNECT;
+			break;
+		}
+	}
+
+	free(resp);
+}
+
+void
+fcgi_create_end_record(struct request *c)
+{
+	struct fcgi_response *resp;
+	struct fcgi_record_header *header;
+	struct fcgi_end_request_body *end_request;
+
+	if ((resp = calloc(1, sizeof(struct fcgi_response))) == NULL) {
+		log_warn("cannot calloc fcgi_response");
+		return;
+	}
+	header = (struct fcgi_record_header*) resp->data;
+	header->version = 1;
+	header->type = FCGI_END_REQUEST;
+	header->id = htons(c->id);
+	header->content_len = htons(sizeof(struct
+	    fcgi_end_request_body));
+	header->padding_len = 0;
+	header->reserved = 0;
+	end_request = (struct fcgi_end_request_body *) (resp->data +
+	    sizeof(struct fcgi_record_header));
+	end_request->app_status = htonl(0); /* script_status */
+	end_request->protocol_status = FCGI_REQUEST_COMPLETE;
+	end_request->reserved[0] = 0;
+	end_request->reserved[1] = 0;
+	end_request->reserved[2] = 0;
+	resp->data_pos = 0;
+	resp->data_len = sizeof(struct fcgi_end_request_body) +
+	    sizeof(struct fcgi_record_header);
+	fcgi_send_response(c, resp);
+}
+
+void
+fcgi_cleanup_request(struct request *c)
+{
+	cgi_inflight--;
+	client_cnt--;
+
+	evtimer_del(&c->tmo);
+	if (event_initialized(&c->ev))
+		event_del(&c->ev);
+
+	close(c->fd);
+	gotweb_free_transport(c->t);
+	free(c);
+}
+
+void
+dump_fcgi_record(const char *p, struct fcgi_record_header *h)
+{
+	dump_fcgi_record_header(p, h);
+
+	if (h->type == FCGI_BEGIN_REQUEST)
+		dump_fcgi_begin_request_body(p,
+		    (struct fcgi_begin_request_body *)(h + 1));
+	else if (h->type == FCGI_END_REQUEST)
+		dump_fcgi_end_request_body(p,
+		    (struct fcgi_end_request_body *)(h + 1));
+}
+
+void
+dump_fcgi_record_header(const char* p, struct fcgi_record_header *h)
+{
+	log_debug("%sversion:         %d", p, h->version);
+	log_debug("%stype:            %d", p, h->type);
+	log_debug("%srequestId:       %d", p, ntohs(h->id));
+	log_debug("%scontentLength:   %d", p, ntohs(h->content_len));
+	log_debug("%spaddingLength:   %d", p, h->padding_len);
+	log_debug("%sreserved:        %d", p, h->reserved);
+}
+
+void
+dump_fcgi_begin_request_body(const char *p, struct fcgi_begin_request_body *b)
+{
+	log_debug("%srole             %d", p, ntohs(b->role));
+	log_debug("%sflags            %d", p, b->flags);
+}
+
+void
+dump_fcgi_end_request_body(const char *p, struct fcgi_end_request_body *b)
+{
+	log_debug("%sappStatus:       %d", p, ntohl(b->app_status));
+	log_debug("%sprotocolStatus:  %d", p, b->protocol_status);
+}
blob - /dev/null
blob + f841f054bc2941b0cdca7e496ea69621671d6766 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/android-chrome-192x192.png differ
blob - /dev/null
blob + 653a1510ce933f7fe9fbab2fcd171f04fa0b24cc (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/android-chrome-384x384.png differ
blob - /dev/null
blob + 460aa1299f8e9f37773618bcab2619794416fb49 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/apple-touch-icon.png differ
blob - /dev/null
blob + b3930d0f047184047cb81d620436d91653438b8b (mode 755)
--- /dev/null
+++ gotwebd/files/htdocs/gotwebd/browserconfig.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<browserconfig>
+    <msapplication>
+        <tile>
+            <square150x150logo src="/mstile-150x150.png"/>
+            <TileColor>#da532c</TileColor>
+        </tile>
+    </msapplication>
+</browserconfig>
blob - /dev/null
blob + f6c1a7c289faa4a48e03c97e68b1ba7a11dfddd1 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/favicon-16x16.png differ
blob - /dev/null
blob + 0ceea8c0eabe73e8d12cf106d73c34abb1999cb2 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/favicon-32x32.png differ
blob - /dev/null
blob + ee414573031ea5b310539196d2530a1e52d49b64 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/favicon.ico differ
blob - /dev/null
blob + 33933f80ee46217039804bc96672ede12b352b93 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/got.png differ
blob - /dev/null
blob + 97ace786464b193baf1cd51e54016aea3016e62f (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/got_large.png differ
blob - /dev/null
blob + 4b9d5d8b3a18a88189f1c92d789387f654bd25ac (mode 755)
--- /dev/null
+++ gotwebd/files/htdocs/gotwebd/gotweb.css
@@ -0,0 +1,792 @@
+/*
+ * Copyright (c) 2019 Jerome Kasper <neon.king.fr@gmail.com>
+ * Copyright (c) 2019, 2020 Tracey Emery <tracey@traceyemery.net>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/* general sections */
+
+a {
+	color: #444444;
+	text-decoration: none;
+}
+a:hover {
+	color: Gold;
+	text-decoration: none;
+}
+body {
+	background-color: #ffffff;
+	color: #000000;
+	margin: 0;
+	padding: 0;
+	font-family: Arial, sans-serif;
+}
+
+.diff_minus, .diff_submodule {
+	color: magenta;
+}
+.diff_plus, .diff_symlink, .diff_author {
+	color: darkcyan;
+}
+.diff_chunk_header, .diff_date {
+	background-color: LightSlateGray;
+	color: yellow;
+}
+.diff_meta, .diff_executable, .diff_commit {
+	color: green;
+}
+.diff_directory {
+	color: blue;
+}
+
+.back_white {
+	background-color: #ffffff;
+}
+.back_lightgray {
+	background-color: #d8f3ef;
+}
+
+#logo {
+	height: 50px;
+}
+#refs_str {
+	background-color: #243647;
+	color: #ffffff;
+	font-style: italic;
+}
+#dotted_line {
+	clear: left;
+	float: left;
+	width: 100%;
+	border-top: 1px dotted #444444;
+}
+#header {
+	overflow: auto;
+	width: 100%;
+	background-image: linear-gradient(to right, White, LightSlateGray);
+}
+#header a {
+	color: #ffffff;
+	font-size: 1.2em;
+	text-decoration: none;
+}
+#header a:hover {
+	color: Gold;
+	font-size: 1.2em;
+	text-decoration: none;
+}
+#site_path {
+	clear: left;
+	float: left;
+	overflow: auto;
+	width: 100%;
+	background-color: #243647;
+}
+#site_link {
+	float: left;
+	width: 40%;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	color: #ffffff;
+	overflow: hidden;
+}
+#site_link a {
+	color: #ffffff;
+	text-decoration: none;
+}
+#search {
+	float: right;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#got_link {
+	float: left;
+	padding-bottom: 10px;
+	padding-top: 10px;
+}
+#content {
+	width: 100%;
+}
+#np_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	border-bottom: 1px dotted #444444;
+	background-color: #f5fcfb;
+	overflow: hidden;
+}
+#nav_prev {
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	overflow: visible;
+}
+#nav_next {
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	text-align: right;
+	overflow: hidden;
+}
+#navs_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: #ced7e0;
+}
+#navs {
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+	font-size: .8em;
+}
+#site_owner_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#site_owner {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#description_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#description {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#repo_owner_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#repo_owner {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#last_change_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#last_change {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#cloneurl_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#cloneurl {
+	float: left;
+	width: 72%;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	overflow: auto;
+	white-space: pre-wrap;
+}
+
+#header_commit_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_commit {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_diff_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_diff {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_author_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_author {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_committer_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_committer {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_age_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_age {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_commit_msg_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_commit_msg {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+	white-space: pre-wrap;
+}
+#header_tree_title {
+	clear: left;
+	float: left;
+	width: 6.5em;
+	padding-left: 10px;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+#header_tree {
+	float: left;
+	width: 72%;
+	padding-top: 2px;
+	padding-bottom: 2px;
+}
+
+#err_content {
+	clear: left;
+	float: left;
+	width: 100%;
+	padding-left: 20px;
+	padding-top: 20px;
+	padding-bottom: 20px;
+}
+
+#briefs_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#briefs_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#briefs_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#briefs_age {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
+}
+#briefs_author {
+	float: left;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	width: 8.5em;
+	font-style: italic;
+	overflow: auto;
+}
+#briefs_log {
+	float: left;
+	padding-left: 10px;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	width: 65%;
+}
+
+#tags_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#tags_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#tags_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#tag_age {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
+}
+#tags_log {
+	float: left;
+	padding-left: 10px;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	width: 65%;
+}
+
+#tag_header_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#tag_header {
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+	width: 80%;
+}
+#tag {
+	float: left;
+	width: 8.5em;
+	font-style: italic;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#tag_commit {
+	clear: left;
+	float: left;
+	padding-left: 20px;
+	padding-bottom: 20px;
+	white-space: pre-wrap;
+}
+
+#index_header {
+	clear: left;
+	float: left;
+	overflow: auto;
+	width: 100%;
+	background-color: Khaki;
+}
+#index_header_project {
+	clear: left;
+	float: left;
+	width: 20%;
+	padding: 10px;
+}
+#index_header_description {
+	float: left;
+	width: 30%;
+	padding: 10px;
+}
+#index_header_owner {
+	float: left;
+	width: 12%;
+	padding: 10px;
+}
+#index_header_age {
+	padding: 10px;
+	overflow: hidden;
+}
+#index_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#index_project {
+	float: left;
+	width: 20%;
+	padding: 10px;
+	overflow: hidden;
+}
+#index_project_description {
+	float: left;
+	width: 30%;
+	padding: 10px;
+	overflow: auto;
+}
+#index_project_owner {
+	float: left;
+	width: 12%;
+	padding: 10px;
+	overflow: hidden;
+}
+#index_project_age {
+	float: left;
+	width: 14%;
+	padding: 10px;
+	overflow: visible;
+}
+#index_project a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_project a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+#index_project_navs a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_project_navs a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+#index_next a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_next a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+#index_prev a {
+	color: #444444;
+	text-decoration: none;
+}
+#index_prev a:hover {
+	color: SteelBlue;
+	text-decoration: none;
+}
+
+#commits_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#commits_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#commits_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#commits_header_wrapper {
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#commits_header {
+	float: left;
+	padding-top: 5px;
+	padding-bottom: 2px;
+	width: 80%;
+}
+#commit {
+	clear: left;
+	float: left;
+	padding-left: 20px;
+	padding-bottom: 20px;
+	white-space: pre-wrap;
+}
+#commits_line {
+	clear: left;
+	float: left;
+}
+
+#blame_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#blame_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#blame_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#blame_header_wrapper {
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#blame_header {
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+	width: 80%;
+}
+#blame {
+	clear: left;
+	float: left;
+	margin-left: 20px;
+	margin-bottom: 20px;
+	font-family: monospace;
+	white-space: pre;
+	overflow: auto;
+}
+#blame_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#blame_number {
+	float: left;
+	width: 6em;
+	overflow: hidden;
+}
+#blame_hash {
+	float: left;
+	width: 6em;
+	overflow: auto;
+}
+#blame_date {
+	float: left;
+	width: 7em;
+	overflow: auto;
+}
+#blame_author {
+	float: left;
+	width: 6em;
+	overflow: hidden;
+}
+#blame_code {
+	float:left;
+	width: 50%;
+	overflow: visible;
+}
+
+#tree_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#tree_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#tree_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#tree_header_wrapper {
+	clear: left;
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#tree_header {
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+	width: 80%;
+}
+#tree {
+	clear: left;
+	float: left;
+	margin-left: 20px;
+	margin-top: 20px;
+	margin-bottom: 20px;
+	font-family: monospace;
+}
+#tree_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#tree_line {
+	clear: left;
+	float: left;
+	width: 20em;
+	padding: 1px;
+}
+#tree_line_blank {
+	float: left;
+	padding: 1px;
+	width: 9.5em;
+}
+#tree_line_navs {
+	float: left;
+	text-align: right;
+	padding: 1px;
+}
+
+#diff_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#diff_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#diff_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#diff_header_wrapper {
+	float: left;
+	background-color: #f5fcfb;
+	width: 100%;
+}
+#diff_header {
+	float: left;
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 2px;
+	width: 80%;
+}
+#diff {
+	clear: left;
+	float: left;
+	margin-left: 20px;
+	margin-bottom: 20px;
+	font-family: monospace;
+	white-space: pre;
+}
+#diff_line {
+	clear: left;
+	float: left;
+}
+
+#summary_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: Khaki;
+}
+
+#branches_title_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+	background-color: LightSlateGray;
+	color: #ffffff;
+}
+#branches_title {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
+#branches_content {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+
+#branches_wrapper {
+	clear: left;
+	float: left;
+	width: 100%;
+}
+#branches_age {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 7.5em;
+	overflow: auto;
+}
+#branches_space {
+	padding-left: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+	float: left;
+	width: 8.5em;
+	overflow: auto;
+}
+#branch {
+	float: left;
+	padding-right: 10px;
+	padding-top: 5px;
+	padding-bottom: 5px;
+}
blob - /dev/null
blob + 0c47027971e9e0a5060e23fe73e7cb0399eacea8 (mode 755)
Binary files /dev/null and gotwebd/files/htdocs/gotwebd/mstile-150x150.png differ
blob - /dev/null
blob + 96e67c7c4b7cb9b1b395281fae8d7cffa834a991 (mode 755)
--- /dev/null
+++ gotwebd/files/htdocs/gotwebd/safari-pinned-tab.svg
@@ -0,0 +1,15 @@
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+ "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
+ width="400.000000pt" height="400.000000pt" viewBox="0 0 400.000000 400.000000"
+ preserveAspectRatio="xMidYMid meet">
+<metadata>
+Created by potrace 1.11, written by Peter Selinger 2001-2013
+</metadata>
+<g transform="translate(0.000000,400.000000) scale(0.100000,-0.100000)"
+fill="#000000" stroke="none">
+<path d="M0 1995 l0 -1215 2000 0 2000 0 0 1215 0 1215 -2000 0 -2000 0 0
+-1215z"/>
+</g>
+</svg>
blob - /dev/null
blob + a1553eb86b573da072c732c9aabac5a80968461f (mode 755)
--- /dev/null
+++ gotwebd/files/htdocs/gotwebd/site.webmanifest
@@ -0,0 +1,19 @@
+{
+    "name": "",
+    "short_name": "",
+    "icons": [
+        {
+            "src": "/android-chrome-192x192.png",
+            "sizes": "192x192",
+            "type": "image/png"
+        },
+        {
+            "src": "/android-chrome-384x384.png",
+            "sizes": "384x384",
+            "type": "image/png"
+        }
+    ],
+    "theme_color": "#ffffff",
+    "background_color": "#ffffff",
+    "display": "standalone"
+}
blob - /dev/null
blob + 6437bf550c8e246193623b0bed1c1356161f0b2e (mode 644)
--- /dev/null
+++ gotwebd/got_operations.c
@@ -0,0 +1,1927 @@
+/*
+ * Copyright (c) 2020-2022 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2018, 2019 Stefan Sperling <stsp@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <event.h>
+#include <imsg.h>
+#include <sha1.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_object.h"
+#include "got_reference.h"
+#include "got_repository.h"
+#include "got_path.h"
+#include "got_cancel.h"
+#include "got_diff.h"
+#include "got_commit_graph.h"
+#include "got_blame.h"
+#include "got_privsep.h"
+
+#include "proc.h"
+#include "gotwebd.h"
+
+static const struct got_error *got_init_repo_commit(struct repo_commit **);
+static const struct got_error *got_init_repo_tag(struct repo_tag **);
+static const struct got_error *got_get_repo_commit(struct request *,
+    struct repo_commit *, struct got_commit_object *, struct got_reflist_head *,
+    struct got_object_id *);
+static const struct got_error *got_gotweb_dupfd(int *, int *);
+static const struct got_error *got_gotweb_openfile(FILE **, int *, int *);
+static const struct got_error *got_gotweb_flushfile(FILE *, int);
+static const struct got_error *got_gotweb_blame_cb(void *, int, int,
+    struct got_commit_object *,struct got_object_id *);
+
+static int
+isbinary(const uint8_t *buf, size_t n)
+{
+	size_t i;
+
+	for (i = 0; i < n; i++)
+		if (buf[i] == 0)
+			return 1;
+	return 0;
+}
+
+
+static const struct got_error *
+got_gotweb_flushfile(FILE *f, int fd)
+{
+	if (fseek(f, 0, SEEK_SET) == -1)
+		return got_error_from_errno("fseek");
+
+	if (ftruncate(fd, 0) == -1)
+		return got_error_from_errno("ftruncate");
+
+	if (fsync(fd) == -1)
+		return got_error_from_errno("fsync");
+
+	if (f && fclose(f) == EOF)
+		return got_error_from_errno("fclose");
+
+	if (fd != -1 && close(fd) != -1)
+		return got_error_from_errno("close");
+
+	return NULL;
+}
+
+static const struct got_error *
+got_gotweb_openfile(FILE **f, int *priv_fd, int *fd)
+{
+	const struct got_error *error = NULL;
+
+	*fd = dup(*priv_fd);
+
+	if (*fd < 0)
+		return NULL;
+
+	*f = fdopen(*fd, "w+");
+	if (*f == NULL) {
+		close(*fd);
+		error = got_error(GOT_ERR_PRIVSEP_NO_FD);
+	}
+
+	return error;
+}
+
+static const struct got_error *
+got_gotweb_dupfd(int *priv_fd, int *fd)
+{
+	const struct got_error *error = NULL;
+
+	*fd = dup(*priv_fd);
+
+	if (*fd < 0)
+		return NULL;
+
+	return error;
+}
+
+const struct got_error *
+got_get_repo_owner(char **owner, struct request *c, char *dir)
+{
+	const struct got_error *error = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	const char *gitconfig_owner;
+
+	*owner = NULL;
+
+	if (srv->show_repo_owner == 0)
+		return NULL;
+
+	gitconfig_owner = got_repo_get_gitconfig_owner(repo);
+	if (gitconfig_owner) {
+		*owner = strdup(gitconfig_owner);
+		if (*owner == NULL)
+			return got_error_from_errno("strdup");
+	}
+	return error;
+}
+
+const struct got_error *
+got_get_repo_age(char **repo_age, struct request *c, char *dir,
+    const char *refname, int ref_tm)
+{
+	const struct got_error *error = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	struct got_commit_object *commit = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	time_t committer_time = 0, cmp_time = 0;
+
+	*repo_age = NULL;
+	TAILQ_INIT(&refs);
+
+	if (srv->show_repo_age == 0)
+		return NULL;
+
+	error = got_ref_list(&refs, repo, "refs/heads",
+	    got_ref_cmp_by_name, NULL);
+	if (error)
+		goto done;
+
+	/*
+	 * Find the youngest branch tip in the repository, or the age of
+	 * the a specific branch tip if a name was provided by the caller.
+	 */
+	TAILQ_FOREACH(re, &refs, entry) {
+		struct got_object_id *id = NULL;
+
+		if (refname && strcmp(got_ref_get_name(re->ref), refname) != 0)
+			continue;
+
+		error = got_ref_resolve(&id, repo, re->ref);
+		if (error)
+			goto done;
+
+		error = got_object_open_as_commit(&commit, repo, id);
+		free(id);
+		if (error)
+			goto done;
+
+		committer_time =
+		    got_object_commit_get_committer_time(commit);
+		got_object_commit_close(commit);
+		if (cmp_time < committer_time)
+			cmp_time = committer_time;
+
+		if (refname)
+			break;
+	}
+
+	if (cmp_time != 0) {
+		committer_time = cmp_time;
+		error = gotweb_get_time_str(repo_age, committer_time, ref_tm);
+	}
+done:
+	got_ref_list_free(&refs);
+	return error;
+}
+
+static const struct got_error *
+got_get_repo_commit(struct request *c, struct repo_commit *repo_commit,
+    struct got_commit_object *commit, struct got_reflist_head *refs,
+    struct got_object_id *id)
+{
+	const struct got_error *error = NULL;
+	struct got_reflist_entry *re;
+	struct got_object_id *id2 = NULL;
+	struct got_object_qid *parent_id;
+	struct transport *t = c->t;
+	struct querystring *qs = c->t->qs;
+	char *commit_msg = NULL, *commit_msg0;
+
+	TAILQ_FOREACH(re, refs, entry) {
+		char *s;
+		const char *name;
+		struct got_tag_object *tag = NULL;
+		struct got_object_id *ref_id;
+		int cmp;
+
+		if (got_ref_is_symbolic(re->ref))
+			continue;
+
+		name = got_ref_get_name(re->ref);
+		if (strncmp(name, "refs/", 5) == 0)
+			name += 5;
+		if (strncmp(name, "got/", 4) == 0)
+			continue;
+		if (strncmp(name, "heads/", 6) == 0)
+			name += 6;
+		if (strncmp(name, "remotes/", 8) == 0) {
+			name += 8;
+			s = strstr(name, "/" GOT_REF_HEAD);
+			if (s != NULL && s[strlen(s)] == '\0')
+				continue;
+		}
+		error = got_ref_resolve(&ref_id, t->repo, re->ref);
+		if (error)
+			return error;
+		if (strncmp(name, "tags/", 5) == 0) {
+			error = got_object_open_as_tag(&tag, t->repo, ref_id);
+			if (error) {
+				if (error->code != GOT_ERR_OBJ_TYPE) {
+					free(ref_id);
+					continue;
+				}
+				/*
+				 * Ref points at something other
+				 * than a tag.
+				 */
+				error = NULL;
+				tag = NULL;
+			}
+		}
+		cmp = got_object_id_cmp(tag ?
+		    got_object_tag_get_object_id(tag) : ref_id, id);
+		free(ref_id);
+		if (tag)
+			got_object_tag_close(tag);
+		if (cmp != 0)
+			continue;
+		s = repo_commit->refs_str;
+		if (asprintf(&repo_commit->refs_str, "%s%s%s", s ? s : "",
+		    s ? ", " : "", name) == -1) {
+			error = got_error_from_errno("asprintf");
+			free(s);
+			repo_commit->refs_str = NULL;
+			return error;
+		}
+		free(s);
+	}
+
+	error = got_object_id_str(&repo_commit->commit_id, id);
+	if (error)
+		return error;
+
+	error = got_object_id_str(&repo_commit->tree_id,
+	    got_object_commit_get_tree_id(commit));
+	if (error)
+		return error;
+
+	if (qs->action == DIFF) {
+		parent_id = STAILQ_FIRST(
+		    got_object_commit_get_parent_ids(commit));
+		if (parent_id != NULL) {
+			id2 = got_object_id_dup(&parent_id->id);
+			error = got_object_id_str(&repo_commit->parent_id, id2);
+			if (error)
+				return error;
+			free(id2);
+		} else {
+			repo_commit->parent_id = strdup("/dev/null");
+			if (repo_commit->parent_id == NULL) {
+				error = got_error_from_errno("strdup");
+				return error;
+			}
+		}
+	}
+
+	repo_commit->committer_time =
+	    got_object_commit_get_committer_time(commit);
+
+	repo_commit->author =
+	    strdup(got_object_commit_get_author(commit));
+	if (repo_commit->author == NULL) {
+		error = got_error_from_errno("strdup");
+		return error;
+	}
+	repo_commit->committer =
+	    strdup(got_object_commit_get_committer(commit));
+	if (repo_commit->committer == NULL) {
+		error = got_error_from_errno("strdup");
+		return error;
+	}
+	error = got_object_commit_get_logmsg(&commit_msg0, commit);
+	if (error)
+		return error;
+
+	commit_msg = commit_msg0;
+	while (*commit_msg == '\n')
+		commit_msg++;
+
+	repo_commit->commit_msg = strdup(commit_msg);
+	if (repo_commit->commit_msg == NULL)
+		error = got_error_from_errno("strdup");
+	free(commit_msg0);
+	return error;
+}
+
+const struct got_error *
+got_get_repo_commits(struct request *c, int limit)
+{
+	const struct got_error *error = NULL;
+	struct got_object_id *id = NULL;
+	struct got_commit_graph *graph = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_reflist_head refs;
+	struct got_reference *ref;
+	struct repo_commit *repo_commit = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = t->repo_dir;
+	char *in_repo_path = NULL, *repo_path = NULL, *file_path = NULL;
+	int chk_next = 0, chk_multi = 0, commit_found = 0;
+	int obj_type, limit_chk = 0;
+
+	TAILQ_INIT(&refs);
+
+	if (qs->file != NULL && strlen(qs->file) > 0)
+		if (asprintf(&file_path, "%s/%s", qs->folder ? qs->folder : "",
+		    qs->file) == -1)
+			return got_error_from_errno("asprintf");
+
+	if (asprintf(&repo_path, "%s/%s", srv->repos_path,
+	    repo_dir->name) == -1)
+		return got_error_from_errno("asprintf");
+
+	error = got_init_repo_commit(&repo_commit);
+	if (error)
+		return error;
+
+	/*
+	 * XXX: jumping directly to a commit id via
+	 * got_repo_match_object_id_prefix significantly improves performance,
+	 * but does not allow us to create a PREVIOUS button, since commits can
+	 * only be itereated forward. So, we have to match as we iterate from
+	 * the headref.
+	 */
+	if (qs->action == BRIEFS || qs->action == COMMITS ||
+	    (qs->action == TREE && qs->commit == NULL)) {
+		error = got_ref_open(&ref, repo, qs->headref, 0);
+		if (error)
+			goto done;
+
+		error = got_ref_resolve(&id, repo, ref);
+		got_ref_close(ref);
+		if (error)
+			goto done;
+	} else if (qs->commit != NULL) {
+		error = got_ref_open(&ref, repo, qs->commit, 0);
+		if (error == NULL) {
+			error = got_ref_resolve(&id, repo, ref);
+			if (error)
+				goto done;
+			error = got_object_get_type(&obj_type, repo, id);
+			got_ref_close(ref);
+			if (error)
+				goto done;
+			if (obj_type == GOT_OBJ_TYPE_TAG) {
+				struct got_tag_object *tag;
+				error = got_object_open_as_tag(&tag, repo, id);
+				if (error)
+					goto done;
+				if (got_object_tag_get_object_type(tag) !=
+				    GOT_OBJ_TYPE_COMMIT) {
+					got_object_tag_close(tag);
+					error = got_error(GOT_ERR_OBJ_TYPE);
+					goto done;
+				}
+				free(id);
+				id = got_object_id_dup(
+				    got_object_tag_get_object_id(tag));
+				if (id == NULL)
+					error = got_error_from_errno(
+					    "got_object_id_dup");
+				got_object_tag_close(tag);
+				if (error)
+					goto done;
+			} else if (obj_type != GOT_OBJ_TYPE_COMMIT) {
+				error = got_error(GOT_ERR_OBJ_TYPE);
+				goto done;
+			}
+		}
+		error = got_repo_match_object_id_prefix(&id, qs->commit,
+		    GOT_OBJ_TYPE_COMMIT, repo);
+		if (error)
+			goto done;
+	}
+
+	error = got_repo_map_path(&in_repo_path, repo, repo_path);
+	if (error)
+		goto done;
+
+	error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name, NULL);
+	if (error)
+		goto done;
+
+	if (qs->file != NULL && strlen(qs->file) > 0) {
+		error = got_commit_graph_open(&graph, file_path, 0);
+		if (error)
+			goto done;
+	} else {
+		error = got_commit_graph_open(&graph, in_repo_path, 0);
+		if (error)
+			goto done;
+	}
+
+	error = got_commit_graph_iter_start(graph, id, repo, NULL, NULL);
+	if (error)
+		goto done;
+
+	for (;;) {
+		if (limit_chk == ((limit * qs->page) - (limit - 1)) &&
+		    commit_found == 0 && repo_commit->commit_id != NULL) {
+			t->prev_id = strdup(repo_commit->commit_id);
+			if (t->prev_id == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		}
+
+		error = got_commit_graph_iter_next(&id, graph, repo, NULL,
+		    NULL);
+		if (error) {
+			if (error->code == GOT_ERR_ITER_COMPLETED)
+				error = NULL;
+			goto done;
+		}
+		if (id == NULL)
+			goto done;
+
+		error = got_object_open_as_commit(&commit, repo, id);
+		if (error)
+			goto done;
+
+		error = got_ref_list(&refs, repo, NULL, got_ref_cmp_by_name,
+		    NULL);
+		if (error)
+			goto done;
+
+		error = got_get_repo_commit(c, repo_commit, commit,
+		    &refs, id);
+		if (error)
+			goto done;
+
+		if (qs->commit != NULL && commit_found == 0 && limit != 1) {
+			if (strcmp(qs->commit, repo_commit->commit_id) == 0)
+				commit_found = 1;
+			else if (qs->file != NULL && strlen(qs->file) > 0 &&
+			    qs->page == 0)
+				commit_found = 1;
+			else {
+				limit_chk++;
+				free(id);
+				id = NULL;
+				continue;
+			}
+		}
+
+		struct repo_commit *new_repo_commit = NULL;
+		error = got_init_repo_commit(&new_repo_commit);
+		if (error)
+			goto done;
+
+		TAILQ_INSERT_TAIL(&t->repo_commits, new_repo_commit, entry);
+
+		error = got_get_repo_commit(c, new_repo_commit, commit,
+		    &refs, id);
+		if (error)
+			goto done;
+
+		free(id);
+		id = NULL;
+
+		if (limit == 1 && chk_multi == 0 &&
+		    srv->max_commits_display != 1)
+			commit_found = 1;
+		else {
+			chk_multi = 1;
+
+			/*
+			 * check for one more commit before breaking,
+			 * so we know whether to navigate through briefs
+			 * commits and summary
+			 */
+			if (chk_next && (qs->action == BRIEFS ||
+			    qs->action == COMMITS || qs->action == SUMMARY)) {
+				t->next_id = strdup(new_repo_commit->commit_id);
+				if (t->next_id == NULL) {
+					error = got_error_from_errno("strdup");
+					goto done;
+				}
+				if (commit) {
+					got_object_commit_close(commit);
+					commit = NULL;
+				}
+				if (t->next_id == NULL) {
+					error = got_error_from_errno("strdup");
+					goto done;
+				}
+				TAILQ_REMOVE(&t->repo_commits, new_repo_commit,
+				    entry);
+				gotweb_free_repo_commit(new_repo_commit);
+				goto done;
+			}
+		}
+		got_ref_list_free(&refs);
+		if (error || (limit && --limit == 0)) {
+			if (commit_found || (qs->file != NULL &&
+			    strlen(qs->file) > 0))
+				if (chk_multi == 0)
+					break;
+			chk_next = 1;
+		}
+		if (commit) {
+			got_object_commit_close(commit);
+			commit = NULL;
+		}
+	}
+done:
+	gotweb_free_repo_commit(repo_commit);
+	if (commit)
+		got_object_commit_close(commit);
+	if (graph)
+		got_commit_graph_close(graph);
+	got_ref_list_free(&refs);
+	free(file_path);
+	free(repo_path);
+	free(id);
+	return error;
+}
+
+const struct got_error *
+got_get_repo_tags(struct request *c, int limit)
+{
+	const struct got_error *error = NULL;
+	struct got_object_id *id = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_reflist_head refs;
+	struct got_reference *ref;
+	struct got_reflist_entry *re;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = t->repo_dir;
+	struct got_tag_object *tag = NULL;
+	struct repo_tag *rt = NULL, *trt = NULL;
+	char *in_repo_path = NULL, *repo_path = NULL, *id_str = NULL;
+	char *commit_msg = NULL, *commit_msg0 = NULL;
+	int chk_next = 0, chk_multi = 1, commit_found = 0, c_cnt = 0;
+
+	TAILQ_INIT(&refs);
+
+	if (asprintf(&repo_path, "%s/%s", srv->repos_path,
+	    repo_dir->name) == -1)
+		return got_error_from_errno("asprintf");
+
+	if (error)
+		return error;
+
+	if (qs->commit == NULL && qs->action == TAGS) {
+		error = got_ref_open(&ref, repo, qs->headref, 0);
+		if (error)
+			goto err;
+		error = got_ref_resolve(&id, repo, ref);
+		got_ref_close(ref);
+		if (error)
+			goto err;
+	} else if (qs->commit == NULL && qs->action == TAG) {
+		error = got_error_msg(GOT_ERR_EOF, "commit id missing");
+		goto err;
+	} else {
+		error = got_repo_match_object_id_prefix(&id, qs->commit,
+		    GOT_OBJ_TYPE_COMMIT, repo);
+		if (error)
+			goto err;
+	}
+
+	if (qs->action != SUMMARY && qs->action != TAGS) {
+		error = got_object_open_as_commit(&commit, repo, id);
+		if (error)
+			goto err;
+		error = got_object_commit_get_logmsg(&commit_msg0, commit);
+		if (error)
+			goto err;
+		if (commit) {
+			got_object_commit_close(commit);
+			commit = NULL;
+		}
+	}
+
+	error = got_repo_map_path(&in_repo_path, repo, repo_path);
+	if (error)
+		goto err;
+
+	error = got_ref_list(&refs, repo, "refs/tags", got_ref_cmp_tags,
+	   repo);
+	if (error)
+		goto err;
+
+	if (limit == 1)
+		chk_multi = 0;
+
+	/*
+	 * XXX: again, see previous message about caching
+	 */
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		struct repo_tag *new_repo_tag = NULL;
+		error = got_init_repo_tag(&new_repo_tag);
+		if (error)
+			goto err;
+
+		TAILQ_INSERT_TAIL(&t->repo_tags, new_repo_tag, entry);
+
+		new_repo_tag->tag_name = strdup(got_ref_get_name(re->ref));
+		if (new_repo_tag->tag_name == NULL) {
+			error = got_error_from_errno("strdup");
+			goto err;
+		}
+
+		error = got_ref_resolve(&id, repo, re->ref);
+		if (error)
+			goto done;
+
+		error = got_object_open_as_tag(&tag, repo, id);
+		if (error) {
+			if (error->code != GOT_ERR_OBJ_TYPE) {
+				free(id);
+				id = NULL;
+				goto done;
+			}
+			/* "lightweight" tag */
+			error = got_object_open_as_commit(&commit, repo, id);
+			if (error) {
+				free(id);
+				id = NULL;
+				goto done;
+			}
+			new_repo_tag->tagger =
+			    strdup(got_object_commit_get_committer(commit));
+			if (new_repo_tag->tagger == NULL) {
+				error = got_error_from_errno("strdup");
+				goto err;
+			}
+			new_repo_tag->tagger_time =
+			    got_object_commit_get_committer_time(commit);
+			error = got_object_id_str(&id_str, id);
+			if (error)
+				goto err;
+			free(id);
+			id = NULL;
+		} else {
+			free(id);
+			id = NULL;
+			new_repo_tag->tagger =
+			    strdup(got_object_tag_get_tagger(tag));
+			if (new_repo_tag->tagger == NULL) {
+				error = got_error_from_errno("strdup");
+				goto err;
+			}
+			new_repo_tag->tagger_time =
+			    got_object_tag_get_tagger_time(tag);
+			error = got_object_id_str(&id_str,
+			    got_object_tag_get_object_id(tag));
+			if (error)
+				goto err;
+		}
+
+		new_repo_tag->commit_id = strdup(id_str);
+		if (new_repo_tag->commit_id == NULL)
+			goto err;
+
+		if (commit_found == 0 && qs->commit != NULL &&
+		    strncmp(id_str, qs->commit, strlen(id_str)) != 0)
+			continue;
+		else
+			commit_found = 1;
+
+		t->tag_count++;
+
+		/*
+		 * check for one more commit before breaking,
+		 * so we know whether to navigate through briefs
+		 * commits and summary
+		 */
+		if (chk_next) {
+			t->next_id = strdup(new_repo_tag->commit_id);
+			if (t->next_id == NULL) {
+				error = got_error_from_errno("strdup");
+				goto err;
+			}
+			if (commit) {
+				got_object_commit_close(commit);
+				commit = NULL;
+			}
+			if (t->next_id == NULL) {
+				error = got_error_from_errno("strdup");
+				goto err;
+			}
+			TAILQ_REMOVE(&t->repo_tags, new_repo_tag, entry);
+			gotweb_free_repo_tag(new_repo_tag);
+			goto done;
+		}
+
+		if (commit) {
+			error = got_object_commit_get_logmsg(&new_repo_tag->
+			    tag_commit, commit);
+			if (error)
+				goto done;
+			got_object_commit_close(commit);
+			commit = NULL;
+		} else {
+			new_repo_tag->tag_commit =
+			    strdup(got_object_tag_get_message(tag));
+			if (new_repo_tag->tag_commit == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		}
+
+		while (*new_repo_tag->tag_commit == '\n')
+			new_repo_tag->tag_commit++;
+
+		if (qs->action != SUMMARY && qs->action != TAGS) {
+			commit_msg = commit_msg0;
+			while (*commit_msg == '\n')
+				commit_msg++;
+
+			new_repo_tag->commit_msg = strdup(commit_msg);
+			if (new_repo_tag->commit_msg == NULL) {
+				error = got_error_from_errno("strdup");
+				free(commit_msg0);
+				goto err;
+			}
+			free(commit_msg0);
+		}
+
+		if (limit && --limit == 0) {
+			if (chk_multi == 0)
+				break;
+			chk_next = 1;
+		}
+		free(id);
+		id = NULL;
+	}
+
+done:
+	/*
+	 * we have tailq populated, so find previous commit id
+	 * for navigation through briefs and commits
+	 */
+	if (t->tag_count == 0) {
+		TAILQ_FOREACH_SAFE(rt, &t->repo_tags, entry, trt) {
+			TAILQ_REMOVE(&t->repo_tags, rt, entry);
+			gotweb_free_repo_tag(rt);
+		}
+	}
+	if (t->tag_count > 0 && t->prev_id == NULL && qs->commit != NULL) {
+		commit_found = 0;
+		TAILQ_FOREACH_REVERSE(rt, &t->repo_tags, repo_tags_head,
+		    entry) {
+			if (commit_found == 0 && rt->commit_id != NULL &&
+			    strcmp(qs->commit, rt->commit_id) != 0) {
+				continue;
+			} else
+				commit_found = 1;
+			if (c_cnt == srv->max_commits_display ||
+			    rt == TAILQ_FIRST(&t->repo_tags)) {
+				t->prev_id = strdup(rt->commit_id);
+				if (t->prev_id == NULL)
+					error = got_error_from_errno("strdup");
+				break;
+			}
+			c_cnt++;
+		}
+	}
+err:
+	if (commit)
+		got_object_commit_close(commit);
+	got_ref_list_free(&refs);
+	free(repo_path);
+	free(id);
+	return error;
+}
+
+const struct got_error *
+got_output_repo_tree(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct got_commit_object *commit = NULL;
+	struct got_repository *repo = t->repo;
+	struct querystring *qs = t->qs;
+	struct repo_commit *rc = NULL;
+	struct got_object_id *tree_id = NULL, *commit_id = NULL;
+	struct got_reflist_head refs;
+	struct got_tree_object *tree = NULL;
+	struct repo_dir *repo_dir = t->repo_dir;
+	char *id_str = NULL;
+	char *path = NULL, *in_repo_path = NULL, *build_folder = NULL;
+	char *modestr = NULL, *name = NULL, *class = NULL;
+	int nentries, i, class_flip = 0;
+
+	TAILQ_INIT(&refs);
+
+	rc = TAILQ_FIRST(&t->repo_commits);
+
+	if (qs->folder != NULL) {
+		path = strdup(qs->folder);
+		if (path == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+	} else {
+		error = got_repo_map_path(&in_repo_path, repo, repo_dir->path);
+		if (error)
+			goto done;
+		free(path);
+		path = in_repo_path;
+	}
+
+	error = got_repo_match_object_id(&commit_id, NULL, rc->commit_id,
+	    GOT_OBJ_TYPE_COMMIT, &refs, repo);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_commit(&commit, repo, commit_id);
+	if (error)
+		goto done;
+
+	error = got_object_id_by_path(&tree_id, repo, commit, path);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_tree(&tree, repo, tree_id);
+	if (error)
+		goto done;
+
+	nentries = got_object_tree_get_nentries(tree);
+
+	for (i = 0; i < nentries; i++) {
+		struct got_tree_entry *te;
+		mode_t mode;
+
+		te = got_object_tree_get_entry(tree, i);
+
+		error = got_object_id_str(&id_str, got_tree_entry_get_id(te));
+		if (error)
+			goto done;
+
+		modestr = strdup("");
+		if (modestr == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+		mode = got_tree_entry_get_mode(te);
+		if (got_object_tree_entry_is_submodule(te)) {
+			free(modestr);
+			modestr = strdup("$");
+			if (modestr == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (S_ISLNK(mode)) {
+			free(modestr);
+			modestr = strdup("@");
+			if (modestr == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (S_ISDIR(mode)) {
+			free(modestr);
+			modestr = strdup("/");
+			if (modestr == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (mode & S_IXUSR) {
+			free(modestr);
+			modestr = strdup("*");
+			if (modestr == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		}
+
+		if (class_flip == 0) {
+			class = strdup("back_lightgray");
+			if (class == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+			class_flip = 1;
+		} else {
+			class = strdup("back_white");
+			if (class == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+			class_flip = 0;
+		}
+
+		name = strdup(got_tree_entry_get_name(te));
+		if (name == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+		if (S_ISDIR(mode)) {
+			if (asprintf(&build_folder, "%s/%s",
+			    qs->folder ? qs->folder : "",
+			    got_tree_entry_get_name(te)) == -1) {
+				error = got_error_from_errno("asprintf");
+				goto done;
+			}
+
+			if (fcgi_gen_response(c,
+			    "<div id='tree_wrapper'>\n") == -1)
+			goto done;
+
+			if (fcgi_gen_response(c, "<div id='tree_line' "
+			    "class='") == -1)
+				goto done;
+			if (fcgi_gen_response(c, class) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "<a class='diff_directory' "
+			    "href='?index_page=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->index_page_str) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "&path=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->path) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "&action=tree") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "&commit=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, rc->commit_id) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "&folder=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, build_folder) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+			if (fcgi_gen_response(c, name) == -1)
+				goto done;
+			if (fcgi_gen_response(c, modestr) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</a>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "<div id='tree_line_blank' "
+			    "class='") == -1)
+				goto done;
+			if (fcgi_gen_response(c, class) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "&nbsp;") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+
+		} else {
+			free(name);
+			name = strdup(got_tree_entry_get_name(te));
+			if (name == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+
+			if (fcgi_gen_response(c,
+			    "<div id='tree_wrapper'>\n") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "<div id='tree_line' "
+			    "class='") == -1)
+				goto done;
+			if (fcgi_gen_response(c, class) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c,
+			    "<a href='?index_page=") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, qs->index_page_str) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&path=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->path) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&action=blob") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&commit=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, rc->commit_id) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&folder=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->folder) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&file=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, name) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+			if (fcgi_gen_response(c, name) == -1)
+				goto done;
+			if (fcgi_gen_response(c, modestr) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "</a>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "<div id='tree_line_blank' "
+			    "class='") == -1)
+				goto done;
+			if (fcgi_gen_response(c, class) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c,
+			    "<a href='?index_page=") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, qs->index_page_str) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&path=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->path) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&action=commits") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&commit=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, rc->commit_id) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&folder=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->folder) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&file=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, name) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "commits") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</a>\n") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, " | \n") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c,
+			    "<a href='?index_page=") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, qs->index_page_str) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&path=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->path) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&action=blame") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&commit=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, rc->commit_id) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&folder=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, qs->folder) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "&file=") == -1)
+				goto done;
+			if (fcgi_gen_response(c, name) == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "'>") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "blame") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</a>\n") == -1)
+				goto done;
+
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+		}
+		free(id_str);
+		id_str = NULL;
+		free(build_folder);
+		build_folder = NULL;
+		free(name);
+		name = NULL;
+		free(modestr);
+		modestr = NULL;
+		free(class);
+		class = NULL;
+	}
+done:
+	free(id_str);
+	free(build_folder);
+	free(modestr);
+	free(path);
+	free(name);
+	free(class);
+	got_ref_list_free(&refs);
+	if (commit)
+		got_object_commit_close(commit);
+	free(commit_id);
+	free(tree_id);
+	return error;
+}
+
+const struct got_error *
+got_output_file_blob(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	struct querystring *qs = c->t->qs;
+	struct got_commit_object *commit = NULL;
+	struct got_object_id *commit_id = NULL;
+	struct got_reflist_head refs;
+	struct got_blob_object *blob = NULL;
+	char *path = NULL, *in_repo_path = NULL;
+	int obj_type, set_mime = 0, type = 0, fd = -1;
+	char *buf_output = NULL;
+	size_t len, hdrlen;
+	const uint8_t *buf;
+
+	TAILQ_INIT(&refs);
+
+	if (asprintf(&path, "%s%s%s", qs->folder ? qs->folder : "",
+	    qs->folder ? "/" : "", qs->file) == -1) {
+		error = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	error = got_repo_map_path(&in_repo_path, repo, path);
+	if (error)
+		goto done;
+
+	error = got_repo_match_object_id(&commit_id, NULL, qs->commit,
+	    GOT_OBJ_TYPE_COMMIT, &refs, repo);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_commit(&commit, repo, commit_id);
+	if (error)
+		goto done;
+
+	error = got_object_id_by_path(&commit_id, repo, commit, in_repo_path);
+	if (error)
+		goto done;
+
+	if (commit_id == NULL) {
+		error = got_error(GOT_ERR_NO_OBJ);
+		goto done;
+	}
+
+	error = got_object_get_type(&obj_type, repo, commit_id);
+	if (error)
+		goto done;
+
+	if (obj_type != GOT_OBJ_TYPE_BLOB) {
+		error = got_error(GOT_ERR_OBJ_TYPE);
+		goto done;
+	}
+
+	error = got_gotweb_dupfd(&c->priv_fd[BLOB_FD_1], &fd);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_blob(&blob, repo, commit_id, BUF, fd);
+	if (error)
+		goto done;
+	hdrlen = got_object_blob_get_hdrlen(blob);
+	do {
+		error = got_object_blob_read_block(&len, blob);
+		if (error)
+			goto done;
+		buf = got_object_blob_get_read_buf(blob);
+
+		/*
+		 * Skip blob object header first time around,
+		 * which also contains a zero byte.
+		 */
+		buf += hdrlen;
+		if (set_mime == 0) {
+			if (isbinary(buf, len - hdrlen)) {
+				error = gotweb_render_content_type_file(c,
+				    "application/octet-stream",
+				    qs->file);
+				if (error) {
+					log_warnx("%s: %s", __func__,
+					    error->msg);
+					goto done;
+				}
+				type = 0;
+			} else {
+				error = gotweb_render_content_type(c,
+				  "text/text");
+				if (error) {
+					log_warnx("%s: %s", __func__,
+					    error->msg);
+					goto done;
+				}
+				type = 1;
+			}
+		}
+		set_mime = 1;
+		if (type) {
+			buf_output = calloc(len - hdrlen + 1,
+			    sizeof(*buf_output));
+			if (buf_output == NULL) {
+				error = got_error_from_errno("calloc");
+				goto done;
+			}
+			memcpy(buf_output, buf, len - hdrlen);
+			fcgi_gen_response(c, buf_output);
+			free(buf_output);
+			buf_output = NULL;
+		} else
+			fcgi_gen_binary_response(c, buf, len - hdrlen);
+
+		hdrlen = 0;
+	} while (len != 0);
+done:
+	if (commit)
+		got_object_commit_close(commit);
+	if (fd != -1 && close(fd) == -1 && error == NULL)
+		error = got_error_from_errno("close");
+	if (blob)
+		got_object_blob_close(blob);
+	free(buf_output);
+	free(in_repo_path);
+	free(commit_id);
+	free(path);
+	return error;
+}
+
+struct blame_line {
+	int annotated;
+	char *id_str;
+	char *committer;
+	char datebuf[11]; /* YYYY-MM-DD + NUL */
+};
+
+struct blame_cb_args {
+	struct blame_line *lines;
+	int nlines;
+	int nlines_prec;
+	int lineno_cur;
+	off_t *line_offsets;
+	FILE *f;
+	struct got_repository *repo;
+	struct request *c;
+};
+
+static const struct got_error *
+got_gotweb_blame_cb(void *arg, int nlines, int lineno,
+    struct got_commit_object *commit, struct got_object_id *id)
+{
+	const struct got_error *err = NULL;
+	struct blame_cb_args *a = arg;
+	struct blame_line *bline;
+	struct request *c = a->c;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = t->repo_dir;
+	char *line = NULL, *eline = NULL;
+	size_t linesize = 0;
+	off_t offset;
+	struct tm tm;
+	time_t committer_time;
+
+	if (nlines != a->nlines ||
+	    (lineno != -1 && lineno < 1) || lineno > a->nlines)
+		return got_error(GOT_ERR_RANGE);
+
+	if (lineno == -1)
+		return NULL; /* no change in this commit */
+
+	/* Annotate this line. */
+	bline = &a->lines[lineno - 1];
+	if (bline->annotated)
+		return NULL;
+	err = got_object_id_str(&bline->id_str, id);
+	if (err)
+		return err;
+
+	bline->committer = strdup(got_object_commit_get_committer(commit));
+	if (bline->committer == NULL) {
+		err = got_error_from_errno("strdup");
+		goto done;
+	}
+
+	committer_time = got_object_commit_get_committer_time(commit);
+	if (gmtime_r(&committer_time, &tm) == NULL)
+		return got_error_from_errno("gmtime_r");
+	if (strftime(bline->datebuf, sizeof(bline->datebuf), "%G-%m-%d",
+	    &tm) == 0) {
+		err = got_error(GOT_ERR_NO_SPACE);
+		goto done;
+	}
+	bline->annotated = 1;
+
+	/* Print lines annotated so far. */
+	bline = &a->lines[a->lineno_cur - 1];
+	if (!bline->annotated)
+		goto done;
+
+	offset = a->line_offsets[a->lineno_cur - 1];
+	if (fseeko(a->f, offset, SEEK_SET) == -1) {
+		err = got_error_from_errno("fseeko");
+		goto done;
+	}
+
+	while (bline->annotated) {
+		int out_buff_size = 100;
+		char *smallerthan, *at, *nl, *committer;
+		char out_buff[out_buff_size];
+		size_t len;
+
+		if (getline(&line, &linesize, a->f) == -1) {
+			if (ferror(a->f))
+				err = got_error_from_errno("getline");
+			break;
+		}
+
+		committer = bline->committer;
+		smallerthan = strchr(committer, '<');
+		if (smallerthan && smallerthan[1] != '\0')
+			committer = smallerthan + 1;
+		at = strchr(committer, '@');
+		if (at)
+			*at = '\0';
+		len = strlen(committer);
+		if (len >= 9)
+			committer[8] = '\0';
+
+		nl = strchr(line, '\n');
+		if (nl)
+			*nl = '\0';
+
+		if (fcgi_gen_response(c, "<div id='blame_wrapper'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='blame_number'>") == -1)
+			goto done;
+		if (snprintf(out_buff, strlen(out_buff), "%.*d", a->nlines_prec,
+		    a->lineno_cur) < 0)
+			goto done;
+		if (fcgi_gen_response(c, out_buff) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='blame_hash'>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=diff&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, bline->id_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (snprintf(out_buff, 10, "%.8s", bline->id_str) < 0)
+			goto done;
+		if (fcgi_gen_response(c, out_buff) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a></div>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='blame_date'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, bline->datebuf) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='blame_author'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, committer) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='blame_code'>") == -1)
+			goto done;
+		err = gotweb_escape_html(&eline, line);
+		if (err)
+			goto done;
+		if (fcgi_gen_response(c, eline) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>") == -1)
+			goto done;
+		a->lineno_cur++;
+		bline = &a->lines[a->lineno_cur - 1];
+	}
+done:
+	free(line);
+	free(eline);
+	return err;
+}
+
+const struct got_error *
+got_output_file_blame(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	struct querystring *qs = c->t->qs;
+	struct got_object_id *obj_id = NULL, *commit_id = NULL;
+	struct got_commit_object *commit = NULL;
+	struct got_reflist_head refs;
+	struct got_blob_object *blob = NULL;
+	char *path = NULL, *in_repo_path = NULL;
+	struct blame_cb_args bca;
+	int i, obj_type, fd1 = -1, fd2 = -1, fd3 = -1, fd4 = -1, fd5 = -1;
+	int fd6 = -1;
+	off_t filesize;
+	FILE *f1 = NULL, *f2 = NULL;
+
+	TAILQ_INIT(&refs);
+	bca.f = NULL;
+	bca.lines = NULL;
+
+	if (asprintf(&path, "%s%s%s", qs->folder ? qs->folder : "",
+	    qs->folder ? "/" : "", qs->file) == -1) {
+		error = got_error_from_errno("asprintf");
+		goto done;
+	}
+
+	error = got_repo_map_path(&in_repo_path, repo, path);
+	if (error)
+		goto done;
+
+	error = got_repo_match_object_id(&commit_id, NULL, qs->commit,
+	    GOT_OBJ_TYPE_COMMIT, &refs, repo);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_commit(&commit, repo, commit_id);
+	if (error)
+		goto done;
+
+	error = got_object_id_by_path(&obj_id, repo, commit, in_repo_path);
+	if (error)
+		goto done;
+
+	if (commit_id == NULL) {
+		error = got_error(GOT_ERR_NO_OBJ);
+		goto done;
+	}
+
+	error = got_object_get_type(&obj_type, repo, obj_id);
+	if (error)
+		goto done;
+
+	if (obj_type != GOT_OBJ_TYPE_BLOB) {
+		error = got_error(GOT_ERR_OBJ_TYPE);
+		goto done;
+	}
+
+	error = got_gotweb_openfile(&bca.f, &c->priv_fd[BLAME_FD_1], &fd1);
+	if (error)
+		goto done;
+
+	error = got_gotweb_dupfd(&c->priv_fd[BLAME_FD_2], &fd2);
+	if (error)
+		goto done;
+
+	error = got_object_open_as_blob(&blob, repo, obj_id, BUF, fd2);
+	if (error)
+		goto done;
+
+	error = got_object_blob_dump_to_file(&filesize, &bca.nlines,
+	    &bca.line_offsets, bca.f, blob);
+	if (error || bca.nlines == 0)
+		goto done;
+
+	/* Don't include \n at EOF in the blame line count. */
+	if (bca.line_offsets[bca.nlines - 1] == filesize)
+		bca.nlines--;
+
+	bca.lines = calloc(bca.nlines, sizeof(*bca.lines));
+	if (bca.lines == NULL) {
+		error = got_error_from_errno("calloc");
+		goto done;
+	}
+	bca.lineno_cur = 1;
+	bca.nlines_prec = 0;
+	i = bca.nlines;
+	while (i > 0) {
+		i /= 10;
+		bca.nlines_prec++;
+	}
+	bca.repo = repo;
+	bca.c = c;
+
+	error = got_gotweb_dupfd(&c->priv_fd[BLAME_FD_3], &fd3);
+	if (error)
+		goto done;
+
+	error = got_gotweb_dupfd(&c->priv_fd[BLAME_FD_4], &fd4);
+	if (error)
+		goto done;
+
+	error = got_gotweb_openfile(&f1, &c->priv_fd[BLAME_FD_5], &fd5);
+	if (error)
+		goto done;
+
+	error = got_gotweb_openfile(&f2, &c->priv_fd[BLAME_FD_6], &fd6);
+	if (error)
+		goto done;
+
+	error = got_blame(in_repo_path, commit_id, repo,
+	    GOT_DIFF_ALGORITHM_MYERS, got_gotweb_blame_cb, &bca, NULL, NULL,
+	    fd3, fd4, f1, f2);
+
+	if (blob) {
+		free(bca.line_offsets);
+		for (i = 0; i < bca.nlines; i++) {
+			struct blame_line *bline = &bca.lines[i];
+			free(bline->id_str);
+			free(bline->committer);
+		}
+	}
+done:
+	free(bca.lines);
+	if (fd2 != -1 && close(fd2) == -1 && error == NULL)
+		error = got_error_from_errno("close");
+	if (fd3 != -1 && close(fd3) == -1 && error == NULL)
+		error = got_error_from_errno("close");
+	if (fd4 != -1 && close(fd4) == -1 && error == NULL)
+		error = got_error_from_errno("close");
+	if (bca.f) {
+		const struct got_error *bca_err =
+		    got_gotweb_flushfile(bca.f, fd1);
+		if (error == NULL)
+			error = bca_err;
+	}
+	if (f1) {
+		const struct got_error *f1_err =
+		    got_gotweb_flushfile(f1, fd5);
+		if (error == NULL)
+			error = f1_err;
+	}
+	if (f2) {
+		const struct got_error *f2_err =
+		    got_gotweb_flushfile(f2, fd6);
+		if (error == NULL)
+			error = f2_err;
+	}
+	if (commit)
+		got_object_commit_close(commit);
+	if (blob)
+		got_object_blob_close(blob);
+	free(in_repo_path);
+	free(commit_id);
+	free(path);
+	return error;
+}
+
+const struct got_error *
+got_output_repo_diff(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct got_repository *repo = t->repo;
+	struct repo_commit *rc = NULL;
+	struct got_object_id *id1 = NULL, *id2 = NULL;
+	struct got_reflist_head refs;
+	FILE *f1 = NULL, *f2 = NULL, *f3 = NULL;
+	char *label1 = NULL, *label2 = NULL, *line = NULL;
+	char *newline, *eline = NULL, *color = NULL;
+	int obj_type, fd1, fd2, fd3, fd4 = -1, fd5 = -1;
+	size_t linesize = 0;
+	ssize_t linelen;
+	int wrlen = 0;
+
+	TAILQ_INIT(&refs);
+
+	error = got_gotweb_openfile(&f1, &c->priv_fd[DIFF_FD_1], &fd1);
+	if (error)
+		return error;
+
+	error = got_gotweb_openfile(&f2, &c->priv_fd[DIFF_FD_2], &fd2);
+	if (error)
+		return error;
+
+	error = got_gotweb_openfile(&f3, &c->priv_fd[DIFF_FD_3], &fd3);
+	if (error)
+		return error;
+
+	rc = TAILQ_FIRST(&t->repo_commits);
+
+	if (rc->parent_id != NULL &&
+	    strncmp(rc->parent_id, "/dev/null", 9) != 0) {
+		error = got_repo_match_object_id(&id1, &label1,
+		    rc->parent_id, GOT_OBJ_TYPE_ANY,
+		    &refs, repo);
+		if (error)
+			goto done;
+	}
+
+	error = got_repo_match_object_id(&id2, &label2, rc->commit_id,
+	    GOT_OBJ_TYPE_ANY, &refs, repo);
+	if (error)
+		goto done;
+
+	error = got_object_get_type(&obj_type, repo, id2);
+	if (error)
+		goto done;
+
+	error = got_gotweb_dupfd(&c->priv_fd[DIFF_FD_4], &fd4);
+	if (error)
+		goto done;
+
+	error = got_gotweb_dupfd(&c->priv_fd[DIFF_FD_5], &fd5);
+	if (error)
+		goto done;
+
+	switch (obj_type) {
+	case GOT_OBJ_TYPE_BLOB:
+		error = got_diff_objects_as_blobs(NULL, NULL, f1, f2, fd4, fd5,
+		     id1, id2, NULL, NULL, GOT_DIFF_ALGORITHM_MYERS, 3, 0, 0,
+		     repo, f3);
+		break;
+	case GOT_OBJ_TYPE_TREE:
+		error = got_diff_objects_as_trees(NULL, NULL, f1, f2, fd4, fd5,
+		    id1, id2, NULL, "", "",  GOT_DIFF_ALGORITHM_MYERS, 3, 0, 0,
+		    repo, f3);
+		break;
+	case GOT_OBJ_TYPE_COMMIT:
+		error = got_diff_objects_as_commits(NULL, NULL, f1, f2, fd4,
+		    fd5, id1, id2, NULL,  GOT_DIFF_ALGORITHM_MYERS, 3, 0, 0,
+		    repo, f3);
+		break;
+	default:
+		error = got_error(GOT_ERR_OBJ_TYPE);
+	}
+	if (error)
+		goto done;
+
+	if (fseek(f1, 0, SEEK_SET) == -1) {
+		error = got_ferror(f1, GOT_ERR_IO);
+		goto done;
+	}
+
+	if (fseek(f2, 0, SEEK_SET) == -1) {
+		error = got_ferror(f2, GOT_ERR_IO);
+		goto done;
+	}
+
+	if (fseek(f3, 0, SEEK_SET) == -1) {
+		error = got_ferror(f3, GOT_ERR_IO);
+		goto done;
+	}
+
+	while ((linelen = getline(&line, &linesize, f3)) != -1) {
+		if (strncmp(line, "-", 1) == 0) {
+			color = strdup("diff_minus");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "+", 1) == 0) {
+			color = strdup("diff_plus");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "@@", 2) == 0) {
+			color = strdup("diff_chunk_header");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "@@", 2) == 0) {
+			color = strdup("diff_chunk_header");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "commit +", 8) == 0) {
+			color = strdup("diff_meta");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "commit -", 8) == 0) {
+			color = strdup("diff_meta");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "blob +", 6) == 0) {
+			color = strdup("diff_meta");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "blob -", 6) == 0) {
+			color = strdup("diff_meta");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "file +", 6) == 0) {
+			color = strdup("diff_meta");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "file -", 6) == 0) {
+			color = strdup("diff_meta");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "from:", 5) == 0) {
+			color = strdup("diff_author");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "via:", 4) == 0) {
+			color = strdup("diff_author");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		} else if (strncmp(line, "date:", 5) == 0) {
+			color = strdup("diff_date");
+			if (color == NULL) {
+				error = got_error_from_errno("strdup");
+				goto done;
+			}
+		}
+		if (fcgi_gen_response(c, "<div id='diff_line' class='") == -1)
+			goto done;
+		if (fcgi_gen_response(c, color ? color : "") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		newline = strchr(line, '\n');
+		if (newline)
+			*newline = '\0';
+
+		error = gotweb_escape_html(&eline, line);
+		if (error)
+			goto done;
+		if (fcgi_gen_response(c, eline) == -1)
+			goto done;
+		free(eline);
+		eline = NULL;
+
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (linelen > 0)
+			wrlen = wrlen + linelen;
+		free(color);
+		color = NULL;
+	}
+	if (linelen == -1 && ferror(f3))
+		error = got_error_from_errno("getline");
+done:
+	free(color);
+	if (fd4 != -1 && close(fd4) == -1 && error == NULL)
+		error = got_error_from_errno("close");
+	if (fd5 != -1 && close(fd5) == -1 && error == NULL)
+		error = got_error_from_errno("close");
+	if (f1) {
+		const struct got_error *f1_err =
+		    got_gotweb_flushfile(f1, fd1);
+		if (error == NULL)
+			error = f1_err;
+	}
+	if (f2) {
+		const struct got_error *f2_err =
+		    got_gotweb_flushfile(f2, fd2);
+		if (error == NULL)
+			error = f2_err;
+	}
+	if (f3) {
+		const struct got_error *f3_err =
+		    got_gotweb_flushfile(f3, fd3);
+		if (error == NULL)
+			error = f3_err;
+	}
+	got_ref_list_free(&refs);
+	free(line);
+	free(eline);
+	free(label1);
+	free(label2);
+	free(id1);
+	free(id2);
+	return error;
+}
+
+static const struct got_error *
+got_init_repo_commit(struct repo_commit **rc)
+{
+	const struct got_error *error = NULL;
+
+	*rc = calloc(1, sizeof(**rc));
+	if (*rc == NULL)
+		return got_error_from_errno2("%s: calloc", __func__);
+
+	(*rc)->path = NULL;
+	(*rc)->refs_str = NULL;
+	(*rc)->commit_id = NULL;
+	(*rc)->committer = NULL;
+	(*rc)->author = NULL;
+	(*rc)->parent_id = NULL;
+	(*rc)->tree_id = NULL;
+	(*rc)->commit_msg = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+got_init_repo_tag(struct repo_tag **rt)
+{
+	const struct got_error *error = NULL;
+
+	*rt = calloc(1, sizeof(**rt));
+	if (*rt == NULL)
+		return got_error_from_errno2("%s: calloc", __func__);
+
+	(*rt)->commit_id = NULL;
+	(*rt)->tag_name = NULL;
+	(*rt)->tag_commit = NULL;
+	(*rt)->commit_msg = NULL;
+	(*rt)->tagger = NULL;
+
+	return error;
+}
blob - /dev/null
blob + 5de75e9906e8db76b4f47ea4e47f97bbff344227 (mode 644)
--- /dev/null
+++ gotwebd/gotweb.c
@@ -0,0 +1,2830 @@
+/*
+ * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2015 Mike Larkin <mlarkin@openbsd.org>
+ * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
+ * Copyright (c) 2013 Florian Obser <florian@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <net/if.h>
+#include <netinet/in.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <dirent.h>
+#include <errno.h>
+#include <event.h>
+#include <imsg.h>
+#include <sha1.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "got_error.h"
+#include "got_object.h"
+#include "got_reference.h"
+#include "got_repository.h"
+#include "got_path.h"
+#include "got_cancel.h"
+#include "got_worktree.h"
+#include "got_diff.h"
+#include "got_commit_graph.h"
+#include "got_blame.h"
+#include "got_privsep.h"
+
+#include "proc.h"
+#include "gotwebd.h"
+
+enum gotweb_ref_tm {
+	TM_DIFF,
+	TM_LONG,
+};
+
+static const struct querystring_keys querystring_keys[] = {
+	{ "action",		ACTION },
+	{ "commit",		COMMIT },
+	{ "file",		RFILE },
+	{ "folder",		FOLDER },
+	{ "headref",		HEADREF },
+	{ "index_page",		INDEX_PAGE },
+	{ "path",		PATH },
+	{ "page",		PAGE },
+};
+
+static const struct action_keys action_keys[] = {
+	{ "blame",	BLAME },
+	{ "blob",	BLOB },
+	{ "briefs",	BRIEFS },
+	{ "commits",	COMMITS },
+	{ "diff",	DIFF },
+	{ "error",	ERR },
+	{ "index",	INDEX },
+	{ "summary",	SUMMARY },
+	{ "tag",	TAG },
+	{ "tags",	TAGS },
+	{ "tree",	TREE },
+};
+
+static const struct got_error *gotweb_init_querystring(struct querystring **);
+static const struct got_error *gotweb_parse_querystring(struct querystring **,
+    char *);
+static const struct got_error *gotweb_assign_querystring(struct querystring **,
+    char *, char *);
+static const struct got_error *gotweb_render_header(struct request *);
+static const struct got_error *gotweb_render_footer(struct request *);
+static const struct got_error *gotweb_render_index(struct request *);
+static const struct got_error *gotweb_init_repo_dir(struct repo_dir **,
+    const char *);
+static const struct got_error *gotweb_load_got_path(struct request *c,
+    struct repo_dir *);
+static const struct got_error *gotweb_get_repo_description(char **,
+    struct server *, char *);
+static const struct got_error *gotweb_get_clone_url(char **, struct server *,
+    char *);
+static const struct got_error *gotweb_render_navs(struct request *);
+static const struct got_error *gotweb_render_blame(struct request *);
+static const struct got_error *gotweb_render_briefs(struct request *);
+static const struct got_error *gotweb_render_commits(struct request *);
+static const struct got_error *gotweb_render_diff(struct request *);
+static const struct got_error *gotweb_render_summary(struct request *);
+static const struct got_error *gotweb_render_tag(struct request *);
+static const struct got_error *gotweb_render_tags(struct request *);
+static const struct got_error *gotweb_render_tree(struct request *);
+static const struct got_error *gotweb_render_branches(struct request *);
+
+static void gotweb_free_querystring(struct querystring *);
+static void gotweb_free_repo_dir(struct repo_dir *);
+
+struct server *gotweb_get_server(uint8_t *, uint8_t *, uint8_t *);
+
+void
+gotweb_process_request(struct request *c)
+{
+	const struct got_error *error = NULL, *error2 = NULL;
+	struct server *srv = NULL;
+	struct querystring *qs = NULL;
+	struct repo_dir *repo_dir = NULL;
+	uint8_t err[] = "gotwebd experienced an error: ";
+	int html = 0;
+
+	/* init the transport */
+	error = gotweb_init_transport(&c->t);
+	if (error) {
+		log_warnx("%s: %s", __func__, error->msg);
+		goto err;
+	}
+	/* don't process any further if client disconnected */
+	if (c->sock->client_status == CLIENT_DISCONNECT)
+		return;
+	/* get the gotwebd server */
+	srv = gotweb_get_server(c->server_name, c->document_root, c->http_host);
+	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) {
+		log_warnx("%s: %s", __func__, error->msg);
+		goto err;
+	}
+	c->t->qs = qs;
+	error = gotweb_parse_querystring(&qs, c->querystring);
+	if (error) {
+		gotweb_free_querystring(qs);
+		log_warnx("%s: %s", __func__, error->msg);
+		goto err;
+	}
+
+	/*
+	 * certain actions require a commit id in the querystring. this stops
+	 * bad actors from exploiting this by manually manipulating the
+	 * querystring.
+	 */
+
+	if (qs->commit == NULL && (qs->action == BLAME || qs->action == BLOB ||
+	    qs->action == DIFF)) {
+		error2 = got_error(GOT_ERR_QUERYSTRING);
+		goto render;
+	}
+
+	if (qs->action != INDEX) {
+		error = gotweb_init_repo_dir(&repo_dir, qs->path);
+		if (error)
+			goto done;
+		error = gotweb_load_got_path(c, repo_dir);
+		c->t->repo_dir = repo_dir;
+		if (error && error->code != GOT_ERR_LONELY_PACKIDX)
+			goto err;
+	}
+
+	/* render top of page */
+	if (qs != NULL && qs->action == BLOB) {
+		error = got_get_repo_commits(c, 1);
+		if (error)
+			goto done;
+		error = gotweb_render_content_type(c, "text/plain");
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		error = got_output_file_blob(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		goto done;
+	} else {
+render:
+		error = gotweb_render_content_type(c, "text/html");
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		html = 1;
+	}
+
+	error = gotweb_render_header(c);
+	if (error) {
+		log_warnx("%s: %s", __func__, error->msg);
+		goto err;
+	}
+
+	if (error2) {
+		error = error2;
+		goto err;
+	}
+
+	switch(qs->action) {
+	case BLAME:
+		error = gotweb_render_blame(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case BRIEFS:
+		error = gotweb_render_briefs(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case COMMITS:
+		error = gotweb_render_commits(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case DIFF:
+		error = gotweb_render_diff(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case INDEX:
+		error = gotweb_render_index(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case SUMMARY:
+		error = gotweb_render_summary(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case TAG:
+		error = gotweb_render_tag(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case TAGS:
+		error = gotweb_render_tags(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case TREE:
+		error = gotweb_render_tree(c);
+		if (error) {
+			log_warnx("%s: %s", __func__, error->msg);
+			goto err;
+		}
+		break;
+	case ERR:
+	default:
+		if (fcgi_gen_response(c, "<div id='err_content'>") == -1)
+			goto err;
+		if (fcgi_gen_response(c, "Error: Bad Querystring\n") == -1)
+			goto err;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto err;
+		break;
+	}
+
+	goto done;
+err:
+	if (html && fcgi_gen_response(c, "<div id='err_content'>") == -1)
+		return;
+	if (fcgi_gen_response(c, err) == -1)
+		return;
+	if (error) {
+		if (fcgi_gen_response(c, (uint8_t *)error->msg) == -1)
+			return;
+	} else {
+		if (fcgi_gen_response(c, "see daemon logs for details") == -1)
+			return;
+	}
+	if (html && fcgi_gen_response(c, "</div>\n") == -1)
+		return;
+done:
+	if (c->t->repo != NULL && qs->action != INDEX)
+		got_repo_close(c->t->repo);
+	if (html && srv != NULL)
+		gotweb_render_footer(c);
+}
+
+struct server *
+gotweb_get_server(uint8_t *server_name, uint8_t *document_root,
+    uint8_t *subdomain)
+{
+	struct server *srv = NULL;
+
+	/* check against document_root first */
+	if (strlen(server_name) > 0)
+		TAILQ_FOREACH(srv, gotwebd_env->servers, entry)
+			if (strcmp(srv->name, server_name) == 0)
+				goto done;
+
+	/* check against document_root second */
+	if (strlen(document_root) > 0)
+		TAILQ_FOREACH(srv, gotwebd_env->servers, entry)
+			if (strcmp(srv->name, document_root) == 0)
+				goto done;
+
+	/* check against subdomain third */
+	if (strlen(subdomain) > 0)
+		TAILQ_FOREACH(srv, gotwebd_env->servers, entry)
+			if (strcmp(srv->name, subdomain) == 0)
+				goto done;
+
+	/* if those fail, send first server */
+	TAILQ_FOREACH(srv, gotwebd_env->servers, entry)
+		if (srv != NULL)
+			break;
+done:
+	return srv;
+};
+
+const struct got_error *
+gotweb_init_transport(struct transport **t)
+{
+	const struct got_error *error = NULL;
+
+	*t = calloc(1, sizeof(**t));
+	if (*t == NULL)
+		return got_error_from_errno2("%s: calloc", __func__);
+
+	TAILQ_INIT(&(*t)->repo_commits);
+	TAILQ_INIT(&(*t)->repo_tags);
+
+	(*t)->repo = NULL;
+	(*t)->repo_dir = NULL;
+	(*t)->qs = NULL;
+	(*t)->next_id = NULL;
+	(*t)->prev_id = NULL;
+	(*t)->next_disp = 0;
+	(*t)->prev_disp = 0;
+
+	return error;
+}
+
+static const struct got_error *
+gotweb_init_querystring(struct querystring **qs)
+{
+	const struct got_error *error = NULL;
+
+	*qs = calloc(1, sizeof(**qs));
+	if (*qs == NULL)
+		return got_error_from_errno2("%s: calloc", __func__);
+
+	(*qs)->action = INDEX;
+	(*qs)->commit = NULL;
+	(*qs)->file = NULL;
+	(*qs)->folder = NULL;
+	(*qs)->headref = strdup("HEAD");
+	if ((*qs)->headref == NULL) {
+		return got_error_from_errno2("%s: strdup", __func__);
+	}
+	(*qs)->index_page = 0;
+	(*qs)->index_page_str = NULL;
+	(*qs)->path = NULL;
+
+	return error;
+}
+
+static const struct got_error *
+gotweb_parse_querystring(struct querystring **qs, char *qst)
+{
+	const struct got_error *error = NULL;
+	char *tok1 = NULL, *tok1_pair = NULL, *tok1_end = NULL;
+	char *tok2 = NULL, *tok2_pair = NULL, *tok2_end = NULL;
+
+	if (qst == NULL)
+		return error;
+
+	tok1 = strdup(qst);
+	if (tok1 == NULL)
+		return got_error_from_errno2("%s: strdup", __func__);
+
+	tok1_pair = tok1;
+	tok1_end = tok1;
+
+	while (tok1_pair != NULL) {
+		strsep(&tok1_end, "&");
+
+		tok2 = strdup(tok1_pair);
+		if (tok2 == NULL) {
+			free(tok1);
+			return got_error_from_errno2("%s: strdup", __func__);
+		}
+
+		tok2_pair = tok2;
+		tok2_end = tok2;
+
+		while (tok2_pair != NULL) {
+			strsep(&tok2_end, "=");
+			if (tok2_end) {
+				error = gotweb_assign_querystring(qs, tok2_pair,
+				    tok2_end);
+				if (error)
+					goto err;
+			}
+			tok2_pair = tok2_end;
+		}
+		free(tok2);
+		tok1_pair = tok1_end;
+	}
+	free(tok1);
+	return error;
+err:
+	free(tok2);
+	free(tok1);
+	return error;
+}
+
+static const struct got_error *
+gotweb_assign_querystring(struct querystring **qs, char *key, char *value)
+{
+	const struct got_error *error = NULL;
+	const char *errstr;
+	int a_cnt, el_cnt;
+
+	for (el_cnt = 0; el_cnt < QSELEM__MAX; el_cnt++) {
+		if (strcmp(key, querystring_keys[el_cnt].name) != 0)
+			continue;
+
+		switch (querystring_keys[el_cnt].element) {
+		case ACTION:
+			for (a_cnt = 0; a_cnt < ACTIONS__MAX; a_cnt++) {
+				if (strcmp(value, action_keys[a_cnt].name) != 0)
+					continue;
+				else if (strcmp(value,
+				    action_keys[a_cnt].name) == 0){
+					(*qs)->action =
+					    action_keys[a_cnt].action;
+					goto qa_found;
+				}
+			}
+			(*qs)->action = ERR;
+qa_found:
+			break;
+		case COMMIT:
+			(*qs)->commit = strdup(value);
+			if ((*qs)->commit == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			break;
+		case RFILE:
+			(*qs)->file = strdup(value);
+			if ((*qs)->file == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			break;
+		case FOLDER:
+			(*qs)->folder = strdup(value);
+			if ((*qs)->folder == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			break;
+		case HEADREF:
+			(*qs)->headref = strdup(value);
+			if ((*qs)->headref == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			break;
+		case INDEX_PAGE:
+			if (strlen(value) == 0)
+				break;
+			(*qs)->index_page_str = strdup(value);
+			if ((*qs)->index_page_str == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			(*qs)->index_page = strtonum(value, INT64_MIN,
+			    INT64_MAX, &errstr);
+			if (errstr) {
+				error = got_error_from_errno3("%s: strtonum %s",
+				    __func__, errstr);
+				goto done;
+			}
+			if ((*qs)->index_page < 0) {
+				(*qs)->index_page = 0;
+				sprintf((*qs)->index_page_str, "%d", 0);
+			}
+			break;
+		case PATH:
+			(*qs)->path = strdup(value);
+			if ((*qs)->path == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			break;
+		case PAGE:
+			if (strlen(value) == 0)
+				break;
+			(*qs)->page_str = strdup(value);
+			if ((*qs)->page_str == NULL) {
+				error = got_error_from_errno2("%s: strdup",
+				    __func__);
+				goto done;
+			}
+			(*qs)->page = strtonum(value, INT64_MIN,
+			    INT64_MAX, &errstr);
+			if (errstr) {
+				error = got_error_from_errno3("%s: strtonum %s",
+				    __func__, errstr);
+				goto done;
+			}
+			if ((*qs)->page < 0) {
+				(*qs)->page = 0;
+				sprintf((*qs)->page_str, "%d", 0);
+			}
+			break;
+		default:
+			break;
+		}
+	}
+done:
+	return error;
+}
+
+void
+gotweb_free_repo_tag(struct repo_tag *rt)
+{
+	if (rt != NULL) {
+		free(rt->commit_msg);
+		free(rt->commit_id);
+		free(rt->tagger);
+	}
+	free(rt);
+}
+
+void
+gotweb_free_repo_commit(struct repo_commit *rc)
+{
+	if (rc != NULL) {
+		free(rc->path);
+		free(rc->refs_str);
+		free(rc->commit_id);
+		free(rc->parent_id);
+		free(rc->tree_id);
+		free(rc->author);
+		free(rc->committer);
+		free(rc->commit_msg);
+	}
+	free(rc);
+}
+
+static void
+gotweb_free_querystring(struct querystring *qs)
+{
+	if (qs != NULL) {
+		free(qs->commit);
+		free(qs->file);
+		free(qs->folder);
+		free(qs->headref);
+		free(qs->index_page_str);
+		free(qs->path);
+		free(qs->page_str);
+	}
+	free(qs);
+}
+
+static void
+gotweb_free_repo_dir(struct repo_dir *repo_dir)
+{
+	if (repo_dir != NULL) {
+		free(repo_dir->name);
+		free(repo_dir->owner);
+		free(repo_dir->description);
+		free(repo_dir->url);
+		free(repo_dir->age);
+		free(repo_dir->path);
+	}
+	free(repo_dir);
+}
+
+void
+gotweb_free_transport(struct transport *t)
+{
+	struct repo_commit *rc = NULL, *trc = NULL;
+	struct repo_tag *rt = NULL, *trt = NULL;
+
+	TAILQ_FOREACH_SAFE(rc, &t->repo_commits, entry, trc) {
+		TAILQ_REMOVE(&t->repo_commits, rc, entry);
+		gotweb_free_repo_commit(rc);
+	}
+	TAILQ_FOREACH_SAFE(rt, &t->repo_tags, entry, trt) {
+		TAILQ_REMOVE(&t->repo_tags, rt, entry);
+		gotweb_free_repo_tag(rt);
+	}
+	gotweb_free_repo_dir(t->repo_dir);
+	gotweb_free_querystring(t->qs);
+	if (t != NULL) {
+		free(t->next_id);
+		free(t->prev_id);
+	}
+	free(t);
+}
+
+const struct got_error *
+gotweb_render_content_type(struct request *c, const uint8_t *type)
+{
+	const struct got_error *error = NULL;
+	char *h = NULL;
+
+	if (asprintf(&h, "Content-type: %s\r\n\r\n", type) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+
+	fcgi_gen_response(c, h);
+done:
+	free(h);
+
+	return error;
+}
+
+const struct got_error *
+gotweb_render_content_type_file(struct request *c, const uint8_t *type,
+    char *file)
+{
+	const struct got_error *error = NULL;
+	char *h = NULL;
+
+	if (asprintf(&h, "Content-type: %s\r\n"
+	    "Content-disposition: attachment; filename=%s\r\n\r\n",
+	    type, file) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+
+	fcgi_gen_response(c, h);
+done:
+	free(h);
+
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_header(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct server *srv = c->srv;
+	struct querystring *qs = c->t->qs;
+	char *title = NULL, *droot = NULL, *css = NULL, *gotlink = NULL;
+	char *gotimg = NULL, *sitelink = NULL, *summlink = NULL;
+
+	if (strlen(c->document_root) > 0) {
+		if (asprintf(&droot, "/%s/", c->document_root) == -1) {
+			error = got_error_from_errno2("%s: asprintf", __func__);
+			goto done;
+		}
+	} else {
+		if (asprintf(&droot, "/") == -1) {
+			error = got_error_from_errno2("%s: asprintf", __func__);
+			goto done;
+		}
+	}
+
+	if (asprintf(&title, "<title>%s</title>\n", srv->site_name) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+	if (asprintf(&css,
+	    "<link rel='stylesheet' type='text/css' href='%s%s'/>\n",
+	    droot, srv->custom_css) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+	if (asprintf(&gotlink, "<a href='%s' target='_sotd'>",
+	    srv->logo_url) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+	if (asprintf(&gotimg, "<img src='%s%s' alt='logo' id='logo'/></a>",
+	    droot, srv->logo) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+	if (asprintf(&sitelink, "<a href='/%s?index_page=%d' "
+	    "alt='sitelink'>%s</a>", c->document_root, qs->index_page,
+	    srv->site_link) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+	if (asprintf(&summlink, "<a href='/%s?index_page=%d&path=%s"
+	    "&action=summary' alt='summlink'>%s</a>", c->document_root,
+	    qs->index_page, qs->path, qs->path) == -1) {
+		error = got_error_from_errno2("%s: asprintf", __func__);
+		goto done;
+	}
+
+	if (fcgi_gen_response(c, "<!DOCTYPE html>\n<head>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, title) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<meta name='viewport' "
+	    "content='initial-scale=.75, user-scalable=yes'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<meta charset='utf-8'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<meta name='msapplication-TileColor' "
+	    "content='#da532c'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<meta name='theme-color' content='#ffffff'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<link rel='apple-touch-icon' sizes='180x180' "
+	    "href='/apple-touch-icon.png'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<link rel='icon' type='image/png' sizes='32x32' "
+	    "href='/favicon-32x32.png'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<link rel='icon' type='image/png' "
+	    "sizes='16x16' href='/favicon-16x16.png'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<link rel='manifest' "
+	    "href='/site.webmanifest'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<link rel='mask-icon' "
+	    "href='/safari-pinned-tab.svg'/>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, css) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</head>\n<body>\n<div id='gw_body'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='header'>\n<div id='got_link'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, gotlink) == -1)
+		goto done;
+	if (fcgi_gen_response(c, gotimg) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='site_path'>\n<div id='site_link'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, sitelink) == -1)
+		goto done;
+	if (qs != NULL) {
+		if (qs->path != NULL) {
+			if (fcgi_gen_response(c, " / ") == -1)
+				goto done;
+			if (fcgi_gen_response(c, summlink) == -1)
+				goto done;
+		}
+		if (qs->action != INDEX) {
+			if (fcgi_gen_response(c, " / ") == -1)
+				goto done;
+			switch(qs->action) {
+			case(BLAME):
+				if (fcgi_gen_response(c, "blame") == -1)
+					goto done;
+				break;
+			case(BRIEFS):
+				if (fcgi_gen_response(c, "briefs") == -1)
+					goto done;
+				break;
+			case(COMMITS):
+				if (fcgi_gen_response(c, "commits") == -1)
+					goto done;
+				break;
+			case(DIFF):
+				if (fcgi_gen_response(c, "diff") == -1)
+					goto done;
+				break;
+			case(SUMMARY):
+				if (fcgi_gen_response(c, "summary") == -1)
+					goto done;
+				break;
+			case(TAG):
+				if (fcgi_gen_response(c, "tag") == -1)
+					goto done;
+				break;
+			case(TAGS):
+				if (fcgi_gen_response(c, "tags") == -1)
+					goto done;
+				break;
+			case(TREE):
+				if (fcgi_gen_response(c, "tree") == -1)
+					goto done;
+				break;
+			default:
+				break;
+			}
+		}
+
+	}
+	fcgi_gen_response(c, "</div>\n</div>\n<div id='content'>\n");
+done:
+	free(title);
+	free(droot);
+	free(css);
+	free(gotlink);
+	free(gotimg);
+	free(sitelink);
+	free(summlink);
+
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_footer(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct server *srv = c->srv;
+	char *siteowner = NULL;
+
+	if (fcgi_gen_response(c, "<div id='site_owner_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='site_owner'>") == -1)
+		goto done;
+	if (srv->show_site_owner) {
+		error = gotweb_escape_html(&siteowner, srv->site_owner);
+		if (error)
+			goto done;
+		if (fcgi_gen_response(c, siteowner) == -1)
+			goto done;
+	} else
+		if (fcgi_gen_response(c, "&nbsp;") == -1)
+			goto done;
+	fcgi_gen_response(c, "</div>\n</div>\n</div>\n</body>\n</html>");
+done:
+	free(siteowner);
+
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_navs(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct server *srv = c->srv;
+	char *nhref = NULL, *phref = NULL;
+	int disp = 0;
+
+	if (fcgi_gen_response(c, "<div id='np_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='nav_prev'>") == -1)
+		goto done;
+
+	switch(qs->action) {
+	case INDEX:
+		if (qs->index_page > 0) {
+			if (asprintf(&phref, "index_page=%d",
+			    qs->index_page - 1) == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	case BRIEFS:
+		if (t->prev_id && qs->commit != NULL &&
+		    strcmp(qs->commit, t->prev_id) != 0) {
+			if (asprintf(&phref, "index_page=%d&path=%s&page=%d"
+			    "&action=briefs&commit=%s&headref=%s",
+			    qs->index_page, qs->path, qs->page - 1, t->prev_id,
+			    qs->headref) == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	case COMMITS:
+		if (t->prev_id && qs->commit != NULL &&
+		    strcmp(qs->commit, t->prev_id) != 0) {
+			if (asprintf(&phref, "index_page=%d&path=%s&page=%d"
+			    "&action=commits&commit=%s&headref=%s&folder=%s"
+			    "&file=%s",
+			    qs->index_page, qs->path, qs->page - 1, t->prev_id,
+			    qs->headref, qs->folder ? qs->folder : "",
+			    qs->file ? qs->file : "") == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	case TAGS:
+		if (t->prev_id && qs->commit != NULL &&
+		    strcmp(qs->commit, t->prev_id) != 0) {
+			if (asprintf(&phref, "index_page=%d&path=%s&page=%d"
+			    "&action=tags&commit=%s&headref=%s",
+			    qs->index_page, qs->path, qs->page - 1, t->prev_id,
+			    qs->headref) == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	default:
+		disp = 0;
+		break;
+	}
+
+	if (disp) {
+		if (fcgi_gen_response(c, "<a href='?") == -1)
+			goto  done;
+		if (fcgi_gen_response(c, phref) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>Previous</a>") == -1)
+			goto done;
+	}
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='nav_next'>") == -1)
+		goto done;
+
+	disp = 0;
+
+	switch(qs->action) {
+	case INDEX:
+		if (t->next_disp == srv->max_repos_display &&
+		    t->repos_total != (qs->index_page + 1) *
+		    srv->max_repos_display) {
+			if (asprintf(&nhref, "index_page=%d",
+			    qs->index_page + 1) == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	case BRIEFS:
+		if (t->next_id) {
+			if (asprintf(&nhref, "index_page=%d&path=%s&page=%d"
+			    "&action=briefs&commit=%s&headref=%s",
+			    qs->index_page, qs->path, qs->page + 1, t->next_id,
+			    qs->headref) == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	case COMMITS:
+		if (t->next_id) {
+			if (asprintf(&nhref, "index_page=%d&path=%s&page=%d"
+			    "&action=commits&commit=%s&headref=%s&folder=%s"
+			    "&file=%s",
+			    qs->index_page, qs->path, qs->page + 1, t->next_id,
+			    qs->headref, qs->folder ? qs->folder : "",
+			    qs->file ? qs->file : "") == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	case TAGS:
+		if (t->next_id) {
+			if (asprintf(&nhref, "index_page=%d&path=%s&page=%d"
+			    "&action=tags&commit=%s&headref=%s",
+			    qs->index_page, qs->path, qs->page + 1, t->next_id,
+			    qs->headref) == -1) {
+				error = got_error_from_errno2("%s: asprintf",
+				    __func__);
+				goto done;
+			}
+			disp = 1;
+		}
+		break;
+	default:
+		disp = 0;
+		break;
+	}
+	if (disp) {
+		if (fcgi_gen_response(c, "<a href='?") == -1)
+			goto done;
+		if (fcgi_gen_response(c, nhref) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>Next</a>") == -1)
+			goto done;
+	}
+	fcgi_gen_response(c, "</div>\n");
+done:
+	free(t->next_id);
+	t->next_id = NULL;
+	free(t->prev_id);
+	t->prev_id = NULL;
+	free(phref);
+	free(nhref);
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_index(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = NULL;
+	DIR *d;
+	struct dirent **sd_dent;
+	char *c_path = NULL;
+	struct stat st;
+	unsigned int d_cnt, d_i, d_disp = 0;
+
+	d = opendir(srv->repos_path);
+	if (d == NULL) {
+		error = got_error_from_errno2("opendir", srv->repos_path);
+		return error;
+	}
+
+	d_cnt = scandir(srv->repos_path, &sd_dent, NULL, alphasort);
+	if (d_cnt == -1) {
+		error = got_error_from_errno2("scandir", srv->repos_path);
+		goto done;
+	}
+
+	/* get total count of repos */
+	for (d_i = 0; d_i < d_cnt; d_i++) {
+		if (strcmp(sd_dent[d_i]->d_name, ".") == 0 ||
+		    strcmp(sd_dent[d_i]->d_name, "..") == 0)
+			continue;
+
+		if (asprintf(&c_path, "%s/%s", srv->repos_path,
+		    sd_dent[d_i]->d_name) == -1) {
+			error = got_error_from_errno("asprintf");
+			return error;
+		}
+
+		if (lstat(c_path, &st) == 0 && S_ISDIR(st.st_mode) &&
+		    !got_path_dir_is_empty(c_path))
+		t->repos_total++;
+		free(c_path);
+		c_path = NULL;
+	}
+
+	if (fcgi_gen_response(c, "<div id='index_header'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='index_header_project'>Project</div>\n") == -1)
+		goto done;
+	if (srv->show_repo_description)
+		if (fcgi_gen_response(c, "<div id='index_header_description'>"
+		    "Description</div>\n") == -1)
+			goto done;
+	if (srv->show_repo_owner)
+		if (fcgi_gen_response(c, "<div id='index_header_owner'>"
+		    "Owner</div>\n") == -1)
+			goto done;
+	if (srv->show_repo_age)
+		if (fcgi_gen_response(c, "<div id='index_header_age'>"
+		    "Last Change</div>\n") == -1)
+			goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	for (d_i = 0; d_i < d_cnt; d_i++) {
+		if (srv->max_repos > 0 && (d_i - 2) == srv->max_repos)
+			break; /* account for parent and self */
+
+		if (strcmp(sd_dent[d_i]->d_name, ".") == 0 ||
+		    strcmp(sd_dent[d_i]->d_name, "..") == 0)
+			continue;
+
+		if (qs->index_page > 0 && (qs->index_page *
+		    srv->max_repos_display) > t->prev_disp) {
+			t->prev_disp++;
+			continue;
+		}
+
+		error = gotweb_init_repo_dir(&repo_dir, sd_dent[d_i]->d_name);
+		if (error)
+			goto done;
+
+		error = gotweb_load_got_path(c, repo_dir);
+		if (error && error->code == GOT_ERR_NOT_GIT_REPO) {
+			error = NULL;
+			continue;
+		}
+		else if (error && error->code != GOT_ERR_LONELY_PACKIDX)
+			goto done;
+
+		if (lstat(repo_dir->path, &st) == 0 &&
+		    S_ISDIR(st.st_mode) &&
+		    !got_path_dir_is_empty(repo_dir->path))
+			goto render;
+		else {
+			gotweb_free_repo_dir(repo_dir);
+			repo_dir = NULL;
+			continue;
+		}
+render:
+		d_disp++;
+		t->prev_disp++;
+		if (fcgi_gen_response(c, "<div id='index_wrapper'>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='index_project'>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=summary'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (srv->show_repo_description) {
+			if (fcgi_gen_response(c,
+			    "<div id='index_project_description'>\n") == -1)
+				goto done;
+			if (fcgi_gen_response(c, repo_dir->description) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+		}
+
+		if (srv->show_repo_owner) {
+			if (fcgi_gen_response(c,
+			    "<div id='index_project_owner'>") == -1)
+				goto done;
+			if (fcgi_gen_response(c, repo_dir->owner) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+		}
+
+		if (srv->show_repo_age) {
+			if (fcgi_gen_response(c,
+			    "<div id='index_project_age'>") == -1)
+				goto done;
+			if (fcgi_gen_response(c, repo_dir->age) == -1)
+				goto done;
+			if (fcgi_gen_response(c, "</div>\n") == -1)
+				goto done;
+		}
+
+		if (fcgi_gen_response(c, "<div id='navs_wrapper'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='navs'>") == -1)
+			goto done;;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=summary'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "summary") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a> | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=briefs'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "commit briefs") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a> | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=commits'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "commits") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a> | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=tags'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "tags") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a> | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=tree'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "tree") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>") == -1)
+			goto done;
+		if (fcgi_gen_response(c,
+		    "<div id='dotted_line'></div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		gotweb_free_repo_dir(repo_dir);
+		repo_dir = NULL;
+		error = got_repo_close(t->repo);
+		if (error)
+			goto done;
+		t->next_disp++;
+		if (d_disp == srv->max_repos_display)
+			break;
+	}
+	if (srv->max_repos_display == 0)
+		goto div;
+	if (srv->max_repos > 0 && srv->max_repos < srv->max_repos_display)
+		goto div;
+	if (t->repos_total <= srv->max_repos ||
+	    t->repos_total <= srv->max_repos_display)
+		goto div;
+
+	error = gotweb_render_navs(c);
+	if (error)
+		goto done;
+div:
+	fcgi_gen_response(c, "</div>\n");
+done:
+	if (d != NULL && closedir(d) == EOF && error == NULL)
+		error = got_error_from_errno("closedir");
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_blame(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct repo_commit *rc = NULL;
+	char *age = NULL;
+
+	error = got_get_repo_commits(c, 1);
+	if (error)
+		return error;
+
+	rc = TAILQ_FIRST(&t->repo_commits);
+
+	error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG);
+	if (error)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='blame_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='blame_title'>Blame</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='blame_content'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='blame_header_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='blame_header'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_age_title'>Date:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_age'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, age ? age : "") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_commit_msg_title'>Message:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_commit_msg'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->commit_msg) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='dotted_line'></div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='blame'>\n") == -1)
+		goto done;
+
+	error = got_output_file_blame(c);
+	if (error)
+		goto done;
+
+	fcgi_gen_response(c, "</div>\n");
+done:
+	fcgi_gen_response(c, "</div>\n");
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_briefs(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct repo_commit *rc = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = t->repo_dir;
+	char *smallerthan, *newline;
+	char *age = NULL;
+
+	if (fcgi_gen_response(c, "<div id='briefs_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='briefs_title'>Commit Briefs</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='briefs_content'>\n") == -1)
+		goto done;
+
+	if (qs->action == SUMMARY) {
+		qs->action = BRIEFS;
+		error = got_get_repo_commits(c, D_MAXSLCOMMDISP);
+	} else
+		error = got_get_repo_commits(c, srv->max_commits_display);
+	if (error)
+		goto done;
+
+	TAILQ_FOREACH(rc, &t->repo_commits, entry) {
+		error = gotweb_get_time_str(&age, rc->committer_time, TM_DIFF);
+		if (error)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='briefs_age'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, age ? age : "") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='briefs_author'>") == -1)
+			goto done;
+		smallerthan = strchr(rc->author, '<');
+		if (smallerthan)
+			*smallerthan = '\0';
+		if (fcgi_gen_response(c, rc->author) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='briefs_log'>") == -1)
+			goto done;
+		newline = strchr(rc->commit_msg, '\n');
+		if (newline)
+			*newline = '\0';
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=diff&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->headref) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_msg) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+		if (rc->refs_str) {
+			if (fcgi_gen_response(c,
+			    " <span id='refs_str'>(") == -1)
+				goto done;
+			if (fcgi_gen_response(c, rc->refs_str) == -1)
+				goto done;
+			if (fcgi_gen_response(c, ")</span>") == -1)
+				goto done;
+		}
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='navs_wrapper'>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='navs'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=diff&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->headref) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "diff") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, " | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=tree&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->headref) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "tree") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c,
+		    "<div id='dotted_line'></div>\n") == -1)
+			goto done;
+
+		free(age);
+		age = NULL;
+	}
+
+	if (t->next_id || t->prev_id) {
+		error = gotweb_render_navs(c);
+		if (error)
+			goto done;
+	}
+	fcgi_gen_response(c, "</div>\n");
+done:
+	free(age);
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_commits(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct repo_commit *rc = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = t->repo_dir;
+	char *age = NULL, *author = NULL;
+	/* int commit_found = 0; */
+
+	if (fcgi_gen_response(c, "<div id='commits_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='commits_title'>Commits</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='commits_content'>\n") == -1)
+		goto done;
+
+	error = got_get_repo_commits(c, srv->max_commits_display);
+	if (error)
+		goto done;
+
+	TAILQ_FOREACH(rc, &t->repo_commits, entry) {
+		error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG);
+		if (error)
+			goto done;
+		error = gotweb_escape_html(&author, rc->author);
+		if (error)
+			goto done;
+
+		if (fcgi_gen_response(c,
+		    "<div id='commits_header_wrapper'>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='commits_header'>\n") == -1)
+			goto done;
+
+
+		if (fcgi_gen_response(c, "<div id='header_commit_title'>Commit:"
+		    "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='header_commit'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='header_author_title'>Author:"
+		    "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='header_author'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, author ? author : "") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='header_age_title'>Date:"
+		    "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='header_age'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, age ? age : "") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c,
+		    "<div id='dotted_line'></div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='commit'>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, rc->commit_msg) == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='navs_wrapper'>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='navs'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=diff&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "diff") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, " | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=tree&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rc->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "tree") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c,
+		    "<div id='dotted_line'></div>\n") == -1)
+			goto done;
+		free(age);
+		age = NULL;
+		free(author);
+		author = NULL;
+	}
+
+	if (t->next_id || t->prev_id) {
+		error = gotweb_render_navs(c);
+		if (error)
+			goto done;
+	}
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	fcgi_gen_response(c, "</div>\n");
+done:
+	free(age);
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_branches(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct got_reflist_head refs;
+	struct got_reflist_entry *re;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct got_repository *repo = t->repo;
+	char *age = NULL;
+
+	TAILQ_INIT(&refs);
+
+	error = got_ref_list(&refs, repo, "refs/heads",
+	    got_ref_cmp_by_name, NULL);
+	if (error)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='branches_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='branches_title'>Branches</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='branches_content'>\n") == -1)
+		goto done;
+
+	TAILQ_FOREACH(re, &refs, entry) {
+		char *refname = NULL;
+
+		if (got_ref_is_symbolic(re->ref))
+			continue;
+
+		refname = strdup(got_ref_get_name(re->ref));
+		if (refname == NULL) {
+			error = got_error_from_errno("strdup");
+			goto done;
+		}
+		if (strncmp(refname, "refs/heads/", 11) != 0)
+			continue;
+
+		error = got_get_repo_age(&age, c, qs->path, refname,
+		    TM_DIFF);
+		if (error)
+			goto done;
+
+		if (strncmp(refname, "refs/heads/", 11) == 0)
+			refname += 11;
+
+		if (fcgi_gen_response(c, "<div id='branches_wrapper'>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='branches_age'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, age ? age : "") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='branches_space'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&nbsp;") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='branch'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->path) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=summary&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, refname) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, refname) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='navs_wrapper'>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='navs'>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->path) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=summary&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, refname) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "summary") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, " | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->path) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=briefs&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, refname) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "commit briefs") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, " | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->path) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=commits&headref=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, refname) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "commits") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c,
+		    "<div id='dotted_line'></div>\n") == -1)
+			goto done;
+
+		free(age);
+		age = NULL;
+
+	}
+	fcgi_gen_response(c, "</div>\n");
+done:
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_tree(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct repo_commit *rc = NULL;
+	char *age = NULL;
+
+	error = got_get_repo_commits(c, 1);
+	if (error)
+		return error;
+
+	rc = TAILQ_FIRST(&t->repo_commits);
+
+	error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG);
+	if (error)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tree_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tree_title'>Tree</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tree_content'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tree_header_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tree_header'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_tree_title'>Tree:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_tree'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->tree_id) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_age_title'>Date:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_age'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, age ? age : "") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_commit_msg_title'>Message:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_commit_msg'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->commit_msg) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='dotted_line'></div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tree'>\n") == -1)
+		goto done;
+
+	error = got_output_repo_tree(c);
+	if (error)
+		goto done;
+
+	fcgi_gen_response(c, "</div>\n");
+	fcgi_gen_response(c, "</div>\n");
+done:
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_diff(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct repo_commit *rc = NULL;
+	char *age = NULL, *author = NULL;
+
+	error = got_get_repo_commits(c, 1);
+	if (error)
+		return error;
+
+	rc = TAILQ_FIRST(&t->repo_commits);
+
+	error = gotweb_get_time_str(&age, rc->committer_time, TM_LONG);
+	if (error)
+		goto done;
+	error = gotweb_escape_html(&author, rc->author);
+	if (error)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='diff_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='diff_title'>Commit Diff</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='diff_content'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='diff_header_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='diff_header'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_diff_title'>Diff:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_diff'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->parent_id) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<br />") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->commit_id) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_commit_title'>Commit:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_commit'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->commit_id) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_tree_title'>Tree:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_tree'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->tree_id) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_author_title'>Author:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_author'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, author ? author : "") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_age_title'>Date:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_age'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, age ? age : "") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_commit_msg_title'>Message:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_commit_msg'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rc->commit_msg) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='dotted_line'></div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='diff'>\n") == -1)
+		goto done;
+
+	error = got_output_repo_diff(c);
+	if (error)
+		goto done;
+
+	fcgi_gen_response(c, "</div>\n");
+done:
+	fcgi_gen_response(c, "</div>\n");
+	free(age);
+	free(author);
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_summary(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct transport *t = c->t;
+	struct server *srv = c->srv;
+
+	if (fcgi_gen_response(c, "<div id='summary_wrapper'>\n") == -1)
+		goto done;
+
+	if (!srv->show_repo_description)
+		goto owner;
+
+	if (fcgi_gen_response(c, "<div id='description_title'>"
+	    "Description:</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='description'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, t->repo_dir->description) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+owner:
+	if (!srv->show_repo_owner)
+		goto last_change;
+
+	if (fcgi_gen_response(c, "<div id='repo_owner_title'>"
+	    "Owner:</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='repo_owner'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, t->repo_dir->owner) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+last_change:
+	if (!srv->show_repo_age)
+		goto clone_url;
+
+	if (fcgi_gen_response(c, "<div id='last_change_title'>"
+	    "Last Change:</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='last_change'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, t->repo_dir->age) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+clone_url:
+	if (!srv->show_repo_cloneurl)
+		goto content;
+
+	if (fcgi_gen_response(c, "<div id='cloneurl_title'>"
+	    "Clone URL:</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='cloneurl'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, t->repo_dir->url) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+content:
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	error = gotweb_render_briefs(c);
+	if (error) {
+		log_warnx("%s: %s", __func__, error->msg);
+		goto done;
+	}
+
+	error = gotweb_render_tags(c);
+	if (error) {
+		log_warnx("%s: %s", __func__, error->msg);
+		goto done;
+	}
+
+	error = gotweb_render_branches(c);
+	if (error)
+		log_warnx("%s: %s", __func__, error->msg);
+done:
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_tag(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct repo_tag *rt = NULL;
+	struct transport *t = c->t;
+	char *age = NULL, *author = NULL;
+
+	error = got_get_repo_tags(c, 1);
+	if (error)
+		goto done;
+
+	if (t->tag_count == 0) {
+		error = got_error_set_errno(GOT_ERR_BAD_OBJ_ID,
+		    "bad commit id");
+		goto done;
+	}
+
+	rt = TAILQ_LAST(&t->repo_tags, repo_tags_head);
+
+	error = gotweb_get_time_str(&age, rt->tagger_time, TM_LONG);
+	if (error)
+		goto done;
+	error = gotweb_escape_html(&author, rt->tagger);
+	if (error)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tags_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tags_title'>Tag</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tags_content'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tag_header_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tag_header'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_commit_title'>Commit:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_commit'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rt->commit_id) == -1)
+		goto done;
+
+	if (strncmp(rt->tag_name, "refs/", 5) == 0)
+		rt->tag_name += 5;
+
+	if (fcgi_gen_response(c, " <span id='refs_str'>(") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rt->tag_name) == -1)
+		goto done;
+	if (fcgi_gen_response(c, ")</span>") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_author_title'>Tagger:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_author'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, author ? author : "") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_age_title'>Date:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_age'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, age ? age : "") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='header_commit_msg_title'>Message:"
+	    "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='header_commit_msg'>") == -1)
+		goto done;
+	if (fcgi_gen_response(c, rt->commit_msg) == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='dotted_line'></div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "<div id='tag_commit'>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, rt->tag_commit) == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+	fcgi_gen_response(c, "</div>\n");
+done:
+	free(age);
+	free(author);
+	return error;
+}
+
+static const struct got_error *
+gotweb_render_tags(struct request *c)
+{
+	const struct got_error *error = NULL;
+	struct repo_tag *rt = NULL;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	struct querystring *qs = t->qs;
+	struct repo_dir *repo_dir = t->repo_dir;
+	char *newline;
+	char *age = NULL;
+	int commit_found = 0;
+
+	if (qs->action == BRIEFS) {
+		qs->action = TAGS;
+		error = got_get_repo_tags(c, D_MAXSLCOMMDISP);
+	} else
+		error = got_get_repo_tags(c, srv->max_commits_display);
+	if (error)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tags_title_wrapper'>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c,
+	    "<div id='tags_title'>Tags</div>\n") == -1)
+		goto done;
+	if (fcgi_gen_response(c, "</div>\n") == -1)
+		goto done;
+
+	if (fcgi_gen_response(c, "<div id='tags_content'>\n") == -1)
+		goto done;
+
+	if (t->tag_count == 0) {
+		if (fcgi_gen_response(c, "<div id='err_content'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c,
+		    "This repository contains no tags\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+	}
+
+	TAILQ_FOREACH(rt, &t->repo_tags, entry) {
+		if (commit_found == 0 && qs->commit != NULL) {
+			if (strcmp(qs->commit, rt->commit_id) != 0)
+				continue;
+			else
+				commit_found = 1;
+		}
+		error = gotweb_get_time_str(&age, rt->tagger_time, TM_DIFF);
+		if (error)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='tag_age'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, age ? age : "") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='tag'>") == -1)
+			goto done;
+		if (strncmp(rt->tag_name, "refs/tags/", 10) == 0)
+			rt->tag_name += 10;
+		if (fcgi_gen_response(c, rt->tag_name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='tags_log'>") == -1)
+			goto done;
+		if (rt->tag_commit != NULL) {
+			newline = strchr(rt->tag_commit, '\n');
+			if (newline)
+				*newline = '\0';
+		}
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=tag&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rt->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (rt->tag_commit != NULL &&
+		    fcgi_gen_response(c, rt->tag_commit) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<div id='navs_wrapper'>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "<div id='navs'>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=tag&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rt->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "tag") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, " | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=briefs&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rt->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "commit briefs") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, " | ") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "<a href='?index_page=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, qs->index_page_str) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&path=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, repo_dir->name) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "&action=commits&commit=") == -1)
+			goto done;
+		if (fcgi_gen_response(c, rt->commit_id) == -1)
+			goto done;
+		if (fcgi_gen_response(c, "'>") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "commits") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</a>") == -1)
+			goto done;
+
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c, "</div>\n") == -1)
+			goto done;
+		if (fcgi_gen_response(c,
+		    "<div id='dotted_line'></div>\n") == -1)
+			goto done;
+
+		free(age);
+		age = NULL;
+	}
+	if (t->next_id || t->prev_id) {
+		error = gotweb_render_navs(c);
+		if (error)
+			goto done;
+	}
+	fcgi_gen_response(c, "</div>\n");
+done:
+	free(age);
+	return error;
+}
+
+const struct got_error *
+gotweb_escape_html(char **escaped_html, const char *orig_html)
+{
+	const struct got_error *error = NULL;
+	struct escape_pair {
+		char c;
+		const char *s;
+	} esc[] = {
+		{ '>', "&gt;" },
+		{ '<', "&lt;" },
+		{ '&', "&amp;" },
+		{ '"', "&quot;" },
+		{ '\'', "&apos;" },
+		{ '\n', "<br />" },
+	};
+	size_t orig_len, len;
+	int i, j, x;
+
+	orig_len = strlen(orig_html);
+	len = orig_len;
+	for (i = 0; i < orig_len; i++) {
+		for (j = 0; j < nitems(esc); j++) {
+			if (orig_html[i] != esc[j].c)
+				continue;
+			len += strlen(esc[j].s) - 1 /* escaped char */;
+		}
+	}
+
+	*escaped_html = calloc(len + 1 /* NUL */, sizeof(**escaped_html));
+	if (*escaped_html == NULL)
+		return got_error_from_errno("calloc");
+
+	x = 0;
+	for (i = 0; i < orig_len; i++) {
+		int escaped = 0;
+		for (j = 0; j < nitems(esc); j++) {
+			if (orig_html[i] != esc[j].c)
+				continue;
+
+			if (strlcat(*escaped_html, esc[j].s, len + 1)
+			    >= len + 1) {
+				error = got_error(GOT_ERR_NO_SPACE);
+				goto done;
+			}
+			x += strlen(esc[j].s);
+			escaped = 1;
+			break;
+		}
+		if (!escaped) {
+			(*escaped_html)[x] = orig_html[i];
+			x++;
+		}
+	}
+done:
+	if (error) {
+		free(*escaped_html);
+		*escaped_html = NULL;
+	} else {
+		(*escaped_html)[x] = '\0';
+	}
+
+	return error;
+}
+
+static const struct got_error *
+gotweb_load_got_path(struct request *c, struct repo_dir *repo_dir)
+{
+	const struct got_error *error = NULL;
+	struct socket *sock = c->sock;
+	struct server *srv = c->srv;
+	struct transport *t = c->t;
+	DIR *dt;
+	char *dir_test;
+	int opened = 0;
+
+	if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, repo_dir->name,
+	    GOTWEB_GIT_DIR) == -1)
+		return got_error_from_errno("asprintf");
+
+	dt = opendir(dir_test);
+	if (dt == NULL) {
+		free(dir_test);
+	} else {
+		repo_dir->path = strdup(dir_test);
+		if (repo_dir->path == NULL) {
+			opened = 1;
+			error = got_error_from_errno("strdup");
+			goto err;
+		}
+		opened = 1;
+		goto done;
+	}
+
+	if (asprintf(&dir_test, "%s/%s/%s", srv->repos_path, repo_dir->name,
+	    GOTWEB_GOT_DIR) == -1) {
+		dir_test = NULL;
+		error = got_error_from_errno("asprintf");
+		goto err;
+	}
+
+	dt = opendir(dir_test);
+	if (dt == NULL)
+		free(dir_test);
+	else {
+		opened = 1;
+		error = got_error(GOT_ERR_NOT_GIT_REPO);
+		goto err;
+	}
+
+	if (asprintf(&dir_test, "%s/%s", srv->repos_path,
+	    repo_dir->name) == -1) {
+		error = got_error_from_errno("asprintf");
+		dir_test = NULL;
+		goto err;
+	}
+
+	repo_dir->path = strdup(dir_test);
+	if (repo_dir->path == NULL) {
+		opened = 1;
+		error = got_error_from_errno("strdup");
+		goto err;
+	}
+
+	dt = opendir(dir_test);
+	if (dt == NULL) {
+		error = got_error_path(repo_dir->name, GOT_ERR_NOT_GIT_REPO);
+		goto err;
+	} else
+		opened = 1;
+done:
+	error = got_repo_open(&t->repo, repo_dir->path, NULL, sock->pack_fds);
+	if (error)
+		goto err;
+	error = gotweb_get_repo_description(&repo_dir->description, srv,
+	    repo_dir->path);
+	if (error)
+		goto err;
+	error = got_get_repo_owner(&repo_dir->owner, c, repo_dir->path);
+	if (error)
+		goto err;
+	error = got_get_repo_age(&repo_dir->age, c, repo_dir->path,
+	    NULL, TM_DIFF);
+	if (error)
+		goto err;
+	error = gotweb_get_clone_url(&repo_dir->url, srv, repo_dir->path);
+err:
+	free(dir_test);
+	if (opened)
+		if (dt != NULL && closedir(dt) == EOF && error == NULL)
+			error = got_error_from_errno("closedir");
+	return error;
+}
+
+static const struct got_error *
+gotweb_init_repo_dir(struct repo_dir **repo_dir, const char *dir)
+{
+	const struct got_error *error;
+
+	*repo_dir = calloc(1, sizeof(**repo_dir));
+	if (*repo_dir == NULL)
+		return got_error_from_errno("calloc");
+
+	if (asprintf(&(*repo_dir)->name, "%s", dir) == -1) {
+		error = got_error_from_errno("asprintf");
+		free(*repo_dir);
+		*repo_dir = NULL;
+		return error;
+	}
+	(*repo_dir)->owner = NULL;
+	(*repo_dir)->description = NULL;
+	(*repo_dir)->url = NULL;
+	(*repo_dir)->age = NULL;
+	(*repo_dir)->path = NULL;
+
+	return NULL;
+}
+
+static const struct got_error *
+gotweb_get_repo_description(char **description, struct server *srv, char *dir)
+{
+	const struct got_error *error = NULL;
+	FILE *f = NULL;
+	char *d_file = NULL;
+	unsigned int len;
+	size_t n;
+
+	*description = NULL;
+	if (srv->show_repo_description == 0)
+		return NULL;
+
+	if (asprintf(&d_file, "%s/description", dir) == -1)
+		return got_error_from_errno("asprintf");
+
+	f = fopen(d_file, "r");
+	if (f == NULL) {
+		if (errno == ENOENT || errno == EACCES)
+			return NULL;
+		error = got_error_from_errno2("fopen", d_file);
+		goto done;
+	}
+
+	if (fseek(f, 0, SEEK_END) == -1) {
+		error = got_ferror(f, GOT_ERR_IO);
+		goto done;
+	}
+	len = ftell(f);
+	if (len == -1) {
+		error = got_ferror(f, GOT_ERR_IO);
+		goto done;
+	}
+
+	if (len == 0)
+		goto done;
+
+	if (fseek(f, 0, SEEK_SET) == -1) {
+		error = got_ferror(f, GOT_ERR_IO);
+		goto done;
+	}
+	*description = calloc(len + 1, sizeof(**description));
+	if (*description == NULL) {
+		error = got_error_from_errno("calloc");
+		goto done;
+	}
+
+	n = fread(*description, 1, len, f);
+	if (n == 0 && ferror(f))
+		error = got_ferror(f, GOT_ERR_IO);
+done:
+	if (f != NULL && fclose(f) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
+	free(d_file);
+	return error;
+}
+
+static const struct got_error *
+gotweb_get_clone_url(char **url, struct server *srv, char *dir)
+{
+	const struct got_error *error = NULL;
+	FILE *f;
+	char *d_file = NULL;
+	unsigned int len;
+	size_t n;
+
+	*url = NULL;
+
+	if (srv->show_repo_cloneurl == 0)
+		return NULL;
+
+	if (asprintf(&d_file, "%s/cloneurl", dir) == -1)
+		return got_error_from_errno("asprintf");
+
+	f = fopen(d_file, "r");
+	if (f == NULL) {
+		if (errno != ENOENT && errno != EACCES)
+			error = got_error_from_errno2("fopen", d_file);
+		goto done;
+	}
+
+	if (fseek(f, 0, SEEK_END) == -1) {
+		error = got_ferror(f, GOT_ERR_IO);
+		goto done;
+	}
+	len = ftell(f);
+	if (len == -1) {
+		error = got_ferror(f, GOT_ERR_IO);
+		goto done;
+	}
+	if (len == 0)
+		goto done;
+
+	if (fseek(f, 0, SEEK_SET) == -1) {
+		error = got_ferror(f, GOT_ERR_IO);
+		goto done;
+	}
+
+	*url = calloc(len + 1, sizeof(**url));
+	if (*url == NULL) {
+		error = got_error_from_errno("calloc");
+		goto done;
+	}
+
+	n = fread(*url, 1, len, f);
+	if (n == 0 && ferror(f))
+		error = got_ferror(f, GOT_ERR_IO);
+done:
+	if (f != NULL && fclose(f) == EOF && error == NULL)
+		error = got_error_from_errno("fclose");
+	free(d_file);
+	return error;
+}
+
+const struct got_error *
+gotweb_get_time_str(char **repo_age, time_t committer_time, int ref_tm)
+{
+	struct tm tm;
+	time_t diff_time;
+	const char *years = "years ago", *months = "months ago";
+	const char *weeks = "weeks ago", *days = "days ago";
+	const char *hours = "hours ago",  *minutes = "minutes ago";
+	const char *seconds = "seconds ago", *now = "right now";
+	char *s;
+	char datebuf[29];
+
+	*repo_age = NULL;
+
+	switch (ref_tm) {
+	case TM_DIFF:
+		diff_time = time(NULL) - committer_time;
+		if (diff_time > 60 * 60 * 24 * 365 * 2) {
+			if (asprintf(repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24 / 365), years) == -1)
+				return got_error_from_errno("asprintf");
+		} else if (diff_time > 60 * 60 * 24 * (365 / 12) * 2) {
+			if (asprintf(repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24 / (365 / 12)),
+			    months) == -1)
+				return got_error_from_errno("asprintf");
+		} else if (diff_time > 60 * 60 * 24 * 7 * 2) {
+			if (asprintf(repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24 / 7), weeks) == -1)
+				return got_error_from_errno("asprintf");
+		} else if (diff_time > 60 * 60 * 24 * 2) {
+			if (asprintf(repo_age, "%lld %s",
+			    (diff_time / 60 / 60 / 24), days) == -1)
+				return got_error_from_errno("asprintf");
+		} else if (diff_time > 60 * 60 * 2) {
+			if (asprintf(repo_age, "%lld %s",
+			    (diff_time / 60 / 60), hours) == -1)
+				return got_error_from_errno("asprintf");
+		} else if (diff_time > 60 * 2) {
+			if (asprintf(repo_age, "%lld %s", (diff_time / 60),
+			    minutes) == -1)
+				return got_error_from_errno("asprintf");
+		} else if (diff_time > 2) {
+			if (asprintf(repo_age, "%lld %s", diff_time,
+			    seconds) == -1)
+				return got_error_from_errno("asprintf");
+		} else {
+			if (asprintf(repo_age, "%s", now) == -1)
+				return got_error_from_errno("asprintf");
+		}
+		break;
+	case TM_LONG:
+		if (gmtime_r(&committer_time, &tm) == NULL)
+			return got_error_from_errno("gmtime_r");
+
+		s = asctime_r(&tm, datebuf);
+		if (s == NULL)
+			return got_error_from_errno("asctime_r");
+
+		if (asprintf(repo_age, "%s UTC", datebuf) == -1)
+			return got_error_from_errno("asprintf");
+		break;
+	}
+	return NULL;
+}
\ No newline at end of file
blob - /dev/null
blob + d73977323f12761149928c250d279753f884a9b3 (mode 644)
--- /dev/null
+++ gotwebd/gotwebd.8
@@ -0,0 +1,152 @@
+.\"
+.\" Copyright (c) 2020 Stefan Sperling
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate$
+.Dt GOTWEB 8
+.Os
+.Sh NAME
+.Nm gotweb
+.Nd Game of Trees Git repository server for web browsers -- which obviously
+needs to be updated to gotwebd
+.Sh SYNOPSIS
+.Nm
+.Sh DESCRIPTION
+.Nm
+provides a web interface allowing Git repository contents to be viewed
+with a web browser.
+.Pp
+.Nm
+is a CGI program based on
+.Xr got 1
+and
+.Xr kcgi 3
+which is intended to run in a
+.Xr chroot 2
+environment in
+.Pa /var/www .
+The program has been designed to work out of the box with
+the
+.Xr httpd 8
+web server in conjunction with
+.Xr slowcgi 8 .
+.Pp
+Enabling
+.Nm
+requires the following steps:
+.Bl -enum
+.It
+The
+.Xr httpd.conf 5
+configuration file must be adjusted to run
+.Nm
+as a CGI program with
+.Xr slowcgi 8 .
+The
+.Sx EXAMPLES
+section below contains an appropriate configuration file sample.
+.It
+httpd(8) and slowcgi(8) must be enabled and started:
+.Bd -literal -offset indent
+  # rcctl enable httpd slowcgi
+  # rcctl start httpd slowcgi
+.Ed
+.It
+Optionally, the run-time behaviour of
+.Nm
+can be configured via the
+.Xr gotweb.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
+.Em not
+be writable by the user ID of the
+.Xr httpd 8
+server.
+The default location for repositories published by
+.Nm
+is
+.Pa /var/www/got/public .
+.It
+Git repositories served by
+.Nm
+should be kept up-to-date with a mechanism such as
+.Cm got fetch ,
+.Xr git-fetch 1 ,
+or
+.Xr rsync 1 ,
+scheduled by
+.Xr cron 8 .
+.El
+.Sh FILES
+.Bl -tag -width /var/www/got/public/ -compact
+.It Pa /var/www/got/public/
+Default location for Git repositories served by
+.Nm .
+This location can be adjusted in the
+.Xr gotweb.conf 5
+configuration file.
+.It Pa /var/www/cgi-bin/gotweb/gotweb
+The
+.Nm
+CGI program, statically linked for use in a
+.Xr chroot 2
+environment.
+.It Pa /var/www/cgi-bin/gotweb/gw_tmpl/
+Directory for template files used by
+.Nm .
+.It Pa /var/www/cgi-bin/gotweb/libexec/
+Directory containing statically linked
+.Xr got 1
+helper programs which are run by
+.Nm
+to read Git repositories.
+.It Pa /var/www/htdocs/gotweb/
+Directory containing HTML, CSS, and image files used by
+.Nm .
+.It Pa /var/www/got/tmp/
+Directory for temporary files created by
+.Nm .
+.El
+.Sh EXAMPLES
+Example configuration for httpd.conf:
+.Bd -literal -offset indent
+
+  types { include "/usr/share/misc/mime.types" }
+  server "gotweb.example.com" {
+  	listen on * port 80
+  	root "/htdocs/gotweb"
+  	location "/cgi-bin/*" {
+  		root "/"
+  		fastcgi
+  	}
+  	location "/*" {
+  		directory index "index.html"
+  	}
+  }
+.Ed
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr kcgi 3 ,
+.Xr git-repository 5 ,
+.Xr gotweb.conf 5 ,
+.Xr httpd 8 ,
+.Xr slowcgi 8
+.Sh AUTHORS
+.An Tracey Emery Aq Mt tracey@traceyemery.net
+.An Stefan Sperling Aq Mt stsp@openbsd.org
blob - /dev/null
blob + c6930e3119c2231e948970c004f3f21d33c34c16 (mode 644)
--- /dev/null
+++ gotwebd/gotwebd.c
@@ -0,0 +1,349 @@
+/*
+ * Copyright (c) 2016, 2019, 2020-2021 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2015 Reyk Floeter <reyk@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/param.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <sys/cdefs.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <termios.h>
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <fcntl.h>
+#include <imsg.h>
+#include <pwd.h>
+#include <signal.h>
+#include <syslog.h>
+#include <unistd.h>
+#include <ctype.h>
+#include <util.h>
+
+#include "got_opentemp.h"
+
+#include "proc.h"
+#include "gotwebd.h"
+
+__dead void usage(void);
+
+int	 main(int, char **);
+int	 gotwebd_configure(struct gotwebd *);
+void	 gotwebd_configure_done(struct gotwebd *);
+void	 gotwebd_sighdlr(int sig, short event, void *arg);
+void	 gotwebd_shutdown(void);
+int	 gotwebd_dispatch_sockets(int, struct privsep_proc *, struct imsg *);
+
+struct gotwebd	*gotwebd_env;
+
+static struct privsep_proc procs[] = {
+	{ "sockets",	PROC_SOCKS,	gotwebd_dispatch_sockets, sockets,
+	    sockets_shutdown },
+};
+
+int
+gotwebd_dispatch_sockets(int fd, struct privsep_proc *p, struct imsg *imsg)
+{
+	struct privsep		*ps = p->p_ps;
+	struct gotwebd		*env = ps->ps_env;
+
+	switch (imsg->hdr.type) {
+	case IMSG_CFG_DONE:
+		gotwebd_configure_done(env);
+		break;
+	default:
+		return (-1);
+	}
+
+	return (0);
+}
+
+void
+gotwebd_sighdlr(int sig, short event, void *arg)
+{
+	/* struct privsep	*ps = arg; */
+
+	if (privsep_process != PROC_GOTWEBD)
+		return;
+
+	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 SIGTERM:
+	case SIGINT:
+		gotwebd_shutdown();
+		break;
+	default:
+		fatalx("unexpected signal");
+	}
+}
+
+__dead void
+usage(void)
+{
+	fprintf(stderr, "usage: %s [-dnv] [-D macro=value] [-f file]\n",
+	    getprogname());
+	exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+	struct gotwebd *env;
+	struct privsep *ps;
+	unsigned int proc;
+	int ch;
+	const char *conffile = GOTWEBD_CONF;
+	enum privsep_procid proc_id = PROC_GOTWEBD;
+	int proc_instance = 0;
+	const char *errp, *title = NULL;
+	int argc0 = argc;
+
+	env = calloc(1, sizeof(*env));
+	if (env == NULL)
+		fatal("%s: calloc", __func__);
+
+	/* XXX: add s and S for both sockets */
+	while ((ch = getopt(argc, argv, "D:P:I:df:vn")) != -1) {
+		switch (ch) {
+		case 'D':
+			if (cmdline_symset(optarg) < 0)
+				log_warnx("could not parse macro definition %s",
+				    optarg);
+			break;
+		case 'd':
+			env->gotwebd_debug = 2;
+			break;
+		case 'f':
+			conffile = optarg;
+			break;
+		case 'v':
+			env->gotwebd_verbose++;
+			break;
+		case 'n':
+			env->gotwebd_debug = 2;
+			env->gotwebd_noaction = 1;
+			break;
+		case 'P':
+			title = optarg;
+			proc_id = proc_getid(procs, nitems(procs), title);
+			if (proc_id == PROC_MAX)
+				fatalx("invalid process name");
+			break;
+		case 'I':
+			proc_instance = strtonum(optarg, 0,
+			    PROC_MAX_INSTANCES, &errp);
+			if (errp)
+				fatalx("invalid process instance");
+			break;
+		default:
+			usage();
+		}
+	}
+
+	/* log to stderr until daemonized */
+	log_init(env->gotwebd_debug ? env->gotwebd_debug : 1, LOG_DAEMON);
+
+	argc -= optind;
+	if (argc > 0)
+		usage();
+
+	ps = calloc(1, sizeof(*ps));
+	if (ps == NULL)
+		fatal("%s: calloc:", __func__);
+
+	gotwebd_env = env;
+	env->gotwebd_ps = ps;
+	ps->ps_env = env;
+	env->gotwebd_conffile = conffile;
+
+	if (parse_config(env->gotwebd_conffile, env) == -1)
+		exit(1);
+
+	if (env->gotwebd_noaction && !env->gotwebd_debug)
+		env->gotwebd_debug = 1;
+
+	/* check for root privileges */
+	if (env->gotwebd_noaction == 0) {
+		if (geteuid())
+			fatalx("need root privileges");
+	}
+
+	ps->ps_pw = getpwnam(GOTWEBD_USER);
+	if (ps->ps_pw == NULL)
+		fatalx("unknown user %s", GOTWEBD_USER);
+
+	log_init(env->gotwebd_debug, LOG_DAEMON);
+	log_setverbose(env->gotwebd_verbose);
+
+	if (env->gotwebd_noaction)
+		ps->ps_noaction = 1;
+
+	ps->ps_instances[PROC_SOCKS] = env->prefork_gotwebd;
+	ps->ps_instance = proc_instance;
+	if (title != NULL)
+		ps->ps_title[proc_id] = title;
+
+	for (proc = 0; proc < nitems(procs); proc++)
+		procs[proc].p_chroot = strlen(env->httpd_chroot) ?
+		    env->httpd_chroot : D_HTTPD_CHROOT;
+
+	/* only the gotwebd returns */
+	proc_init(ps, procs, nitems(procs), argc0, argv, proc_id);
+
+	log_procinit("gotwebd");
+	if (!env->gotwebd_debug && daemon(0, 0) == -1)
+		fatal("can't daemonize");
+
+	if (ps->ps_noaction == 0)
+		log_info("%s startup", getprogname());
+
+	event_init();
+
+	signal_set(&ps->ps_evsigint, SIGINT, gotwebd_sighdlr, ps);
+	signal_set(&ps->ps_evsigterm, SIGTERM, gotwebd_sighdlr, ps);
+	signal_set(&ps->ps_evsighup, SIGHUP, gotwebd_sighdlr, ps);
+	signal_set(&ps->ps_evsigpipe, SIGPIPE, gotwebd_sighdlr, ps);
+	signal_set(&ps->ps_evsigusr1, SIGUSR1, gotwebd_sighdlr, ps);
+
+	signal_add(&ps->ps_evsigint, NULL);
+	signal_add(&ps->ps_evsigterm, NULL);
+	signal_add(&ps->ps_evsighup, NULL);
+	signal_add(&ps->ps_evsigpipe, NULL);
+	signal_add(&ps->ps_evsigusr1, NULL);
+
+	if (!env->gotwebd_noaction)
+		proc_connect(ps);
+
+	if (gotwebd_configure(env) == -1)
+		fatalx("configuration failed");
+
+#ifdef PROFILE
+	if (unveil("gmon.out", "rwc") != 0)
+		err(1, "gmon.out");
+#endif
+
+	if (unveil(strlen(env->httpd_chroot) > 0 ? env->httpd_chroot :
+	    D_HTTPD_CHROOT, "rwc") == -1)
+		err(1, "unveil");
+
+	if (unveil(GOT_TMPDIR_STR, "rw") == -1)
+		err(1, "unveil");
+
+	if (unveil(GOTWEBD_CONF, "r") == -1)
+		err(1, "unveil");
+
+	if (unveil(NULL, NULL) != 0)
+		err(1, "unveil");
+
+#ifndef PROFILE
+	if (pledge("stdio rpath wpath cpath inet unix", NULL) == -1)
+		err(1, "pledge");
+#endif
+
+	event_dispatch();
+
+	log_debug("%s gotwebd exiting", getprogname());
+
+	return (0);
+}
+
+int
+gotwebd_configure(struct gotwebd *env)
+{
+	struct server *srv;
+	struct socket *sock;
+	int id;
+
+	if (env->gotwebd_noaction) {
+		fprintf(stderr, "configuration OK\n");
+		proc_kill(env->gotwebd_ps);
+		exit(0);
+	}
+
+	/* gotweb need to reload its config. */
+	env->gotwebd_reload = 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__);
+	}
+
+	/* send our sockets */
+	TAILQ_FOREACH(sock, env->sockets, entry) {
+		if (config_setsock(env, sock) == -1)
+			fatalx("%s: send socket error", __func__);
+		if (config_setfd(env, sock) == -1)
+			fatalx("%s: send priv_fd error", __func__);
+	}
+
+	for (id = 0; id < PROC_MAX; id++) {
+		if (id == privsep_process)
+			continue;
+		proc_compose(env->gotwebd_ps, id, IMSG_CFG_DONE, NULL, 0);
+	}
+
+	return (0);
+}
+
+void
+gotwebd_configure_done(struct gotwebd *env)
+{
+	int id;
+
+	if (env->gotwebd_reload == 0) {
+		log_warnx("%s: configuration already finished", __func__);
+		return;
+	}
+
+	env->gotwebd_reload--;
+	if (env->gotwebd_reload == 0) {
+		for (id = 0; id < PROC_MAX; id++) {
+			if (id == privsep_process)
+				continue;
+			proc_compose(env->gotwebd_ps, id, IMSG_CTL_START,
+			    NULL, 0);
+		}
+	}
+}
+
+void
+gotwebd_shutdown(void)
+{
+	proc_kill(gotwebd_env->gotwebd_ps);
+
+	/* unlink(gotwebd_env->gotweb->gotweb_conf.gotweb_unix_socket_name); */
+	/* free(gotwebd_env->gotweb); */
+	free(gotwebd_env);
+
+	log_warnx("gotwebd terminating");
+	exit(0);
+}
blob - /dev/null
blob + 7dba54734ea8a323a1f5597f698f84a790722531 (mode 644)
--- /dev/null
+++ gotwebd/gotwebd.conf.5
@@ -0,0 +1,135 @@
+.\"
+.\" Copyright (c) 2020 Tracey Emery <tracey@traceyemery.net>
+.\"
+.\" Permission to use, copy, modify, and distribute this software for any
+.\" purpose with or without fee is hereby granted, provided that the above
+.\" copyright notice and this permission notice appear in all copies.
+.\"
+.\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+.\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+.\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+.\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+.\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+.\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+.\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+.\"
+.Dd $Mdocdate$
+.Dt GOTWEB.CONF 5
+.Os
+.Sh NAME
+.Nm gotweb.conf
+.Nd gotweb configuration file
+.Sh DESCRIPTION
+.Nm
+is the run-time configuration file for
+.Xr gotweb 8 .
+.Pp
+The file format is line-based, with one configuration directive per line.
+Any lines beginning with a
+.Sq #
+are treated as comments and ignored.
+.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 configuration directives are as follows:
+.Bl -tag -width Ds
+.It Ic got_max_commits_display Ar number
+Set the maximum amount of commits displayed per page.
+.It Ic got_logo Ar path
+Set the path to an image file containing a logo to be displayed.
+.It Ic got_logo_url Ar url
+Set a hyperlink for the logo.
+.It Ic got_max_repos Ar number
+Set the maximum amount of repositories
+.Xr gotweb 8
+will work with.
+.It Ic got_max_repos_display Ar number
+Set the maximum amount of repositories displayed on the index screen.
+.It Ic got_show_repo_age Ar on | off
+Toggle display of last repository modification date.
+.It Ic got_show_repo_cloneurl Ar on | off
+Toggle display of clone URLs for a repository.
+This requires the creation of a
+.Pa cloneurl
+file inside the repository which contains one URL per line.
+.It Ic got_show_repo_description Ar on | off
+Toggle display of the repository description.
+The
+.Pa description
+file in the repository should be updated with an appropriate description.
+.It Ic got_repos_path Ar path
+Set the path to the directory which contains Git repositories that
+.Xr gotweb 8
+should publish.
+.It Ic got_show_repo_owner Ar on | off
+Set whether to display the repository owner.
+Displaying the owner requires owner information to be added to the
+.Pa config
+file in the repository.
+.Xr gotweb 8
+will parse owner information from either a [gotweb] or a [gitweb] section.
+For example:
+.Bd -literal -offset indent
+[gotweb]
+owner = "Your Name"
+.Ed
+.It Ic got_site_link Ar string
+Set the displayed site link name for the index page.
+.It Ic got_site_name Ar string
+Set the displayed site name title.
+.It Ic got_site_owner Ar string
+Set the displayed site owner.
+.It Ic got_show_site_owner Ar on | off
+Toggle display of the site owner.
+.It Ic got_www_path Ar string
+Set the public gotweb httpd path.
+.El
+.Sh EXAMPLES
+These are the currently configurable items for
+.Xr gotweb 8
+with their default values.
+.Bd -literal -offset indent
+
+#
+# gotweb options
+# all paths relative to /var/www (httpd chroot jail)
+#
+
+got_repos_path   "/got/public"
+got_www_path   "/gotweb"
+
+#got_max_repos   100
+#got_max_repos_display  25
+got_max_commits_display  50
+
+got_site_name   "my public repos"
+got_site_owner   "Got Owner"
+got_site_link   "repos"
+
+got_logo   "got.png"
+got_logo_url   "https://gameoftrees.org"
+
+# on by default
+#got_show_site_owner  off
+#got_show_repo_owner  off
+#got_show_repo_age  false
+#got_show_repo_description no
+#got_show_repo_cloneurl  off
+.Ed
+.Sh FILES
+.Bl -tag -width Ds -compact
+.It Pa /var/www/etc/gotweb.conf
+Location of the
+.Nm
+configuration file.
+.El
+.Sh SEE ALSO
+.Xr got 1 ,
+.Xr gotweb 8
blob - /dev/null
blob + 38c0c930eedf474fcef860b7a5336ec4ae64724d (mode 644)
--- /dev/null
+++ gotwebd/gotwebd.h
@@ -0,0 +1,468 @@
+/*
+ * Copyright (c) 2016, 2019, 2020-2022 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2015 Mike Larkin <mlarkin@openbsd.org>
+ * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
+ * Copyright (c) 2013 Florian Obser <florian@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <netinet/in.h>
+#include <net/if.h>
+#include <sys/queue.h>
+
+#include <limits.h>
+#include <stdio.h>
+
+#ifdef DEBUG
+#define dprintf(x...)   do { log_debug(x); } while(0)
+#else
+#define dprintf(x...)
+#endif /* DEBUG */
+
+#ifndef nitems
+#define nitems(_a)	(sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+/* GOTWEBD DEFAULTS */
+#define GOTWEBD_CONF		 "/etc/gotwebd.conf"
+
+#define GOTWEBD_USER		 "www"
+
+#define GOTWEBD_MAXCLIENTS	 1024
+#define GOTWEBD_MAXTEXT		 511
+#define GOTWEBD_MAXNAME		 64
+#define GOTWEBD_MAXPORT		 6
+#define GOTWEBD_NUMPROC		 3
+#define GOTWEBD_MAXIFACE	 16
+
+/* GOTWEB DEFAULTS */
+#define MAX_QUERYSTRING		 2048
+#define MAX_DOCUMENT_ROOT	 255
+#define MAX_SERVER_NAME		 255
+
+#define GOTWEB_GOT_DIR		 ".got"
+#define GOTWEB_GIT_DIR		 ".git"
+
+#define D_HTTPD_CHROOT		 "/var/www"
+#define D_UNIX_SOCKET		 "/run/gotweb.sock"
+#define D_FCGI_PORT		 "9000"
+#define D_GOTPATH		 "/got/public"
+#define D_SITENAME		 "Gotweb"
+#define D_SITEOWNER		 "Got Owner"
+#define D_SITELINK		 "Repos"
+#define D_GOTLOGO		 "got.png"
+#define D_GOTURL		 "https://gameoftrees.org"
+#define D_GOTWEBCSS		 "gotweb.css"
+
+#define D_SHOWROWNER		 1
+#define D_SHOWSOWNER		 1
+#define D_SHOWAGE		 1
+#define D_SHOWDESC		 1
+#define D_SHOWURL		 1
+#define D_MAXREPO		 0
+#define D_MAXREPODISP		 25
+#define D_MAXSLCOMMDISP		 10
+#define D_MAXCOMMITDISP		 25
+
+#define BUF			 8192
+
+#define TIMEOUT_DEFAULT		 120
+
+#define FCGI_CONTENT_SIZE	 65535
+#define FCGI_PADDING_SIZE	 255
+#define FCGI_RECORD_SIZE	 \
+    (sizeof(struct fcgi_record_header) + FCGI_CONTENT_SIZE + FCGI_PADDING_SIZE)
+
+#define FCGI_ALIGNMENT		 8
+#define FCGI_ALIGN(n)		 \
+    (((n) + (FCGI_ALIGNMENT - 1)) & ~(FCGI_ALIGNMENT - 1))
+
+#define FD_RESERVE		 5
+#define FD_NEEDED		 6
+
+#define FCGI_BEGIN_REQUEST	 1
+#define FCGI_ABORT_REQUEST	 2
+#define FCGI_END_REQUEST	 3
+#define FCGI_PARAMS		 4
+#define FCGI_STDIN		 5
+#define FCGI_STDOUT		 6
+#define FCGI_STDERR		 7
+#define FCGI_DATA		 8
+#define FCGI_GET_VALUES		 9
+#define FCGI_GET_VALUES_RESULT	10
+#define FCGI_UNKNOWN_TYPE	11
+#define FCGI_MAXTYPE		(FCGI_UNKNOWN_TYPE)
+
+#define FCGI_REQUEST_COMPLETE	0
+#define FCGI_CANT_MPX_CONN	1
+#define FCGI_OVERLOADED		2
+#define FCGI_UNKNOWN_ROLE	3
+
+/* XXX: move this later after ig */
+#define GOT_PACK_NUM_TEMPFILES     32
+
+enum imsg_type {
+	IMSG_CFG_SRV = IMSG_PROC_MAX,
+	IMSG_CFG_SOCK,
+	IMSG_CFG_FD,
+	IMSG_CFG_DONE,
+	IMSG_CTL_START,
+};
+
+struct env_val {
+	SLIST_ENTRY(env_val)	 entry;
+	char			*val;
+};
+SLIST_HEAD(env_head, env_val);
+
+struct fcgi_record_header {
+	uint8_t		version;
+	uint8_t		type;
+	uint16_t	id;
+	uint16_t	content_len;
+	uint8_t		padding_len;
+	uint8_t		reserved;
+}__packed;
+
+struct fcgi_response {
+	TAILQ_ENTRY(fcgi_response)	entry;
+	uint8_t				data[FCGI_RECORD_SIZE];
+	size_t				data_pos;
+	size_t				data_len;
+};
+
+struct repo_dir {
+	char			*name;
+	char			*owner;
+	char			*description;
+	char			*url;
+	char			*age;
+	char			*path;
+};
+
+struct repo_tag {
+	TAILQ_ENTRY(repo_tag)	 entry;
+	char			*commit_id;
+	char			*tag_name;
+	char			*tag_commit;
+	char			*commit_msg;
+	char			*tagger;
+	time_t			 tagger_time;
+};
+
+struct repo_commit {
+	TAILQ_ENTRY(repo_commit)	 entry;
+	char			*path;
+	char			*refs_str;
+	char			*commit_id; /* id_str1 */
+	char			*parent_id; /* id_str2 */
+	char			*tree_id;
+	char			*author;
+	char			*committer;
+	char			*commit_msg;
+	time_t			 committer_time;
+};
+
+struct got_repository;
+struct transport {
+	TAILQ_HEAD(repo_commits_head, repo_commit)	 repo_commits;
+	TAILQ_HEAD(repo_tags_head, repo_tag)		 repo_tags;
+	struct got_repository	*repo;
+	struct repo_dir		*repo_dir;
+	struct querystring	*qs;
+	char			*next_id;
+	char			*prev_id;
+	unsigned int		 repos_total;
+	unsigned int		 next_disp;
+	unsigned int		 prev_disp;
+	unsigned int		 tag_count;
+};
+
+enum socket_priv_fds {
+	DIFF_FD_1,
+	DIFF_FD_2,
+	DIFF_FD_3,
+	DIFF_FD_4,
+	DIFF_FD_5,
+	BLAME_FD_1,
+	BLAME_FD_2,
+	BLAME_FD_3,
+	BLAME_FD_4,
+	BLAME_FD_5,
+	BLAME_FD_6,
+	BLOB_FD_1,
+	BLOB_FD_2,
+	PRIV_FDS__MAX,
+};
+
+struct request {
+	struct socket			*sock;
+	struct server			*srv;
+	struct transport		*t;
+	struct event			 ev;
+	struct event			 tmo;
+
+	uint16_t			 id;
+	int				 fd;
+	int				 priv_fd[PRIV_FDS__MAX];
+
+	uint8_t				 buf[FCGI_RECORD_SIZE];
+	size_t				 buf_pos;
+	size_t				 buf_len;
+
+	char				 querystring[MAX_QUERYSTRING];
+	char				 http_host[GOTWEBD_MAXTEXT];
+	char				 document_root[MAX_DOCUMENT_ROOT];
+	char				 server_name[MAX_SERVER_NAME];
+
+	struct env_head			 env;
+	int				 env_count;
+
+	uint8_t				 request_started;
+};
+
+struct fcgi_begin_request_body {
+	uint16_t	role;
+	uint8_t		flags;
+	uint8_t		reserved[5];
+}__packed;
+
+struct fcgi_end_request_body {
+	uint32_t	app_status;
+	uint8_t		protocol_status;
+	uint8_t		reserved[3];
+}__packed;
+
+struct address {
+	TAILQ_ENTRY(address)	 entry;
+	struct sockaddr_storage	 ss;
+	int			 ipproto;
+	int			 prefixlen;
+	in_port_t		 port;
+	char			 ifname[IFNAMSIZ];
+};
+TAILQ_HEAD(addresslist, address);
+
+struct server {
+	TAILQ_ENTRY(server)	 entry;
+	struct addresslist	*al;
+
+	char		 name[GOTWEBD_MAXTEXT];
+
+	char		 repos_path[PATH_MAX];
+	char		 site_name[GOTWEBD_MAXNAME];
+	char		 site_owner[GOTWEBD_MAXNAME];
+	char		 site_link[GOTWEBD_MAXTEXT];
+	char		 logo[GOTWEBD_MAXTEXT];
+	char		 logo_url[GOTWEBD_MAXTEXT];
+	char		 custom_css[PATH_MAX];
+
+	size_t		 max_repos;
+	size_t		 max_repos_display;
+	size_t		 max_commits_display;
+
+	int		 show_site_owner;
+	int		 show_repo_owner;
+	int		 show_repo_age;
+	int		 show_repo_description;
+	int		 show_repo_cloneurl;
+
+	int		 unix_socket;
+	char		 unix_socket_name[PATH_MAX];
+
+	int		 fcgi_socket;
+	char		 fcgi_socket_bind[GOTWEBD_MAXTEXT];
+	in_port_t	 fcgi_socket_port;
+};
+TAILQ_HEAD(serverlist, server);
+
+enum client_action {
+	CLIENT_CONNECT,
+	CLIENT_DISCONNECT,
+};
+
+enum sock_type {
+	UNIX,
+	FCGI,
+};
+
+struct socket_conf {
+	struct addresslist	*al;
+
+	char		 name[GOTWEBD_MAXTEXT];
+	char		 srv_name[GOTWEBD_MAXTEXT];
+
+	int		 id;
+	int		 child_id;
+	int		 parent_id;
+
+	int		 ipv4;
+	int		 ipv6;
+
+	int		 type;
+	char		 unix_socket_name[PATH_MAX];
+	in_port_t	 fcgi_socket_port;
+};
+
+struct socket {
+	TAILQ_ENTRY(socket)	 entry;
+	struct socket_conf	 conf;
+
+	int		 fd;
+	int		 pack_fds[GOT_PACK_NUM_TEMPFILES];
+	int		 priv_fd[PRIV_FDS__MAX];
+
+	struct event	 evt;
+	struct event	 ev;
+	struct event	 pause;
+
+	int		 client_status;
+};
+TAILQ_HEAD(socketlist, socket);
+
+struct gotwebd {
+	struct serverlist	*servers;
+	struct socketlist	*sockets;
+
+	struct privsep	*gotwebd_ps;
+	const char	*gotwebd_conffile;
+
+	int		 gotwebd_debug;
+	int		 gotwebd_verbose;
+	int		 gotwebd_noaction;
+
+	uint16_t	 prefork_gotwebd;
+	int		 gotwebd_reload;
+
+	int		 server_cnt;
+
+	char		 httpd_chroot[PATH_MAX];
+
+	int		 unix_socket;
+	char		 unix_socket_name[PATH_MAX];
+
+	int		 fcgi_socket;
+	char		 fcgi_socket_bind[GOTWEBD_MAXTEXT];
+	in_port_t	 fcgi_socket_port;
+};
+
+struct querystring {
+	uint8_t		 action;
+	char		*commit;
+	char		*previd;
+	char		*prevset;
+	char		*file;
+	char		*folder;
+	char		*headref;
+	int		 index_page;
+	char		*index_page_str;
+	char		*path;
+	int		 page;
+	char		*page_str;
+};
+
+struct querystring_keys {
+	const char	*name;
+	int		 element;
+};
+
+struct action_keys {
+	const char	*name;
+	int		 action;
+};
+
+enum querystring_elements {
+	ACTION,
+	COMMIT,
+	RFILE,
+	FOLDER,
+	HEADREF,
+	INDEX_PAGE,
+	PATH,
+	PAGE,
+	PREVID,
+	QSELEM__MAX,
+};
+
+enum query_actions {
+	BLAME,
+	BLOB,
+	BRIEFS,
+	COMMITS,
+	DIFF,
+	ERR,
+	INDEX,
+	SUMMARY,
+	TAG,
+	TAGS,
+	TREE,
+	ACTIONS__MAX,
+};
+
+extern struct gotwebd	*gotwebd_env;
+
+/* sockets.c */
+void sockets(struct privsep *, struct privsep_proc *);
+void sockets_shutdown(void);
+void sockets_parse_sockets(struct gotwebd *);
+void sockets_socket_accept(int, short, void *);
+int sockets_privinit(struct gotwebd *, struct socket *);
+
+/* gotweb.c */
+const struct got_error *gotweb_render_content_type(struct request *,
+    const uint8_t *);
+const struct got_error
+    *gotweb_render_content_type_file(struct request *, const uint8_t *, char *);
+const struct got_error *gotweb_get_time_str(char **, time_t, int);
+const struct got_error *gotweb_init_transport(struct transport **);
+const struct got_error *gotweb_escape_html(char **, const char *);
+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 *);
+
+/* parse.y */
+int parse_config(const char *, struct gotwebd *);
+int cmdline_symset(char *);
+
+/* fcgi.c */
+void fcgi_request(int, short, void *);
+void fcgi_timeout(int, short, void *);
+void fcgi_cleanup_request(struct request *);
+void fcgi_create_end_record(struct request *);
+void dump_fcgi_record(const char *, struct fcgi_record_header *);
+int fcgi_gen_response(struct request *, const char *);
+int fcgi_gen_binary_response(struct request *, const uint8_t *, int);
+
+/* got_operations.c */
+const struct got_error *got_get_repo_owner(char **, struct request *, char *);
+const struct got_error *got_get_repo_age(char **, struct request *, char *,
+    const char *, int);
+const struct got_error *got_get_repo_commits(struct request *, int);
+const struct got_error *got_get_repo_tags(struct request *, int);
+const struct got_error *got_get_repo_heads(struct request *);
+const struct got_error *got_output_repo_diff(struct request *);
+const struct got_error *got_output_repo_tree(struct request *);
+const struct got_error *got_output_file_blob(struct request *);
+const struct got_error *got_output_file_blame(struct request *);
+
+/* 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_getsock(struct gotwebd *, struct imsg *);
+int config_setfd(struct gotwebd *, struct socket *);
+int config_getfd(struct gotwebd *, struct imsg *);
+int config_getcfg(struct gotwebd *, struct imsg *);
+int config_init(struct gotwebd *);
blob - /dev/null
blob + 5fd34708bd3654bc05060446ff5d55557747cfd3 (mode 644)
--- /dev/null
+++ gotwebd/libexec/Makefile
@@ -0,0 +1,4 @@
+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 - /dev/null
blob + 85bee26728643214f5d4570f003572ac1fc36d05 (mode 644)
--- /dev/null
+++ gotwebd/libexec/Makefile.inc
@@ -0,0 +1,11 @@
+.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 - /dev/null
blob + 8a3f38ce4c45ce1386bdc180e342b043f8abe809 (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-blob/Makefile
@@ -0,0 +1,16 @@
+
+.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 sha1.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 - /dev/null
blob + 0996068f0f3d0bb71779d03c1c1a7ec355644166 (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-commit/Makefile
@@ -0,0 +1,16 @@
+
+.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 sha1.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 - /dev/null
blob + 77cc7852cae9199c991f2908f2d9e8a27da650ee (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-gitconfig/Makefile
@@ -0,0 +1,16 @@
+
+.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 sha1.c gitconfig.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 - /dev/null
blob + 29605918a07b9e1969e3a2722605c3424e77d13f (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-gotconfig/Makefile
@@ -0,0 +1,17 @@
+
+.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 sha1.c parse.y
+
+CPPFLAGS = -I${.CURDIR}/../../../include -I${.CURDIR}/../../../lib \
+	-I${.CURDIR}/../../../libexec/got-read-gotconfig
+LDADD = -lutil -lz
+DPADD = ${LIBZ} ${LIBUTIL}
+LDSTATIC = ${STATIC}
+
+.PATH:	${.CURDIR}/../../../lib ${.CURDIR}/../../../libexec/got-read-gotconfig
+
+.include <bsd.prog.mk>
blob - /dev/null
blob + 4889fe0bab46bbcdb20610ea9255916cd11d0e0d (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-object/Makefile
@@ -0,0 +1,16 @@
+
+.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 sha1.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 - /dev/null
blob + a28b9cfd2d1c5d17ffcbe771790c2183e406f924 (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-pack/Makefile
@@ -0,0 +1,17 @@
+
+.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 sha1.c delta_cache.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 - /dev/null
blob + 3a0b798c57ea849bde67efe66e3b721f7486e287 (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-tag/Makefile
@@ -0,0 +1,16 @@
+
+.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 sha1.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 - /dev/null
blob + 19a4c9cfa3379ca5fec4fafdc691d38c80c996e8 (mode 644)
--- /dev/null
+++ gotwebd/libexec/got-read-tree/Makefile
@@ -0,0 +1,16 @@
+
+.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 sha1.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 - /dev/null
blob + 79d3d334581eddd8ffcc0f02f067e5c64d0be72e (mode 644)
--- /dev/null
+++ gotwebd/log.c
@@ -0,0 +1,214 @@
+/*
+ * Copyright (c) 2003, 2004 Henning Brauer <henning@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <syslog.h>
+#include <errno.h>
+#include <time.h>
+
+static int	 debug;
+static int	 verbose;
+const char	*log_procname;
+
+void	log_init(int, int);
+void	log_procinit(const char *);
+void	log_setverbose(int);
+int	log_getverbose(void);
+void	log_warn(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_warnx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_info(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_debug(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	logit(int, const char *, ...)
+	    __attribute__((__format__ (printf, 2, 3)));
+void	vlog(int, const char *, va_list)
+	    __attribute__((__format__ (printf, 2, 0)));
+__dead void fatal(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+__dead void fatalx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+
+void
+log_init(int n_debug, int facility)
+{
+	debug = n_debug;
+	verbose = n_debug;
+	log_procinit(getprogname());
+
+	if (!debug)
+		openlog(getprogname(), LOG_PID | LOG_NDELAY, facility);
+
+	tzset();
+}
+
+void
+log_procinit(const char *procname)
+{
+	if (procname != NULL)
+		log_procname = procname;
+}
+
+void
+log_setverbose(int v)
+{
+	verbose = v;
+}
+
+int
+log_getverbose(void)
+{
+	return (verbose);
+}
+
+void
+logit(int pri, const char *fmt, ...)
+{
+	va_list ap;
+
+	va_start(ap, fmt);
+	vlog(pri, fmt, ap);
+	va_end(ap);
+}
+
+void
+vlog(int pri, const char *fmt, va_list ap)
+{
+	char *nfmt;
+	int saved_errno = errno;
+
+	if (debug) {
+		/* best effort in out of mem situations */
+		if (asprintf(&nfmt, "%s\n", fmt) == -1) {
+			vfprintf(stderr, fmt, ap);
+			fprintf(stderr, "\n");
+		} else {
+			vfprintf(stderr, nfmt, ap);
+			free(nfmt);
+		}
+		fflush(stderr);
+	} else
+		vsyslog(pri, fmt, ap);
+
+	errno = saved_errno;
+}
+
+void
+log_warn(const char *emsg, ...)
+{
+	char *nfmt;
+	va_list ap;
+	int saved_errno = errno;
+
+	/* best effort to even work in out of memory situations */
+	if (emsg == NULL)
+		logit(LOG_CRIT, "%s", strerror(saved_errno));
+	else {
+		va_start(ap, emsg);
+
+		if (asprintf(&nfmt, "%s: %s", emsg,
+		    strerror(saved_errno)) == -1) {
+			/* we tried it... */
+			vlog(LOG_CRIT, emsg, ap);
+			logit(LOG_CRIT, "%s", strerror(saved_errno));
+		} else {
+			vlog(LOG_CRIT, nfmt, ap);
+			free(nfmt);
+		}
+		va_end(ap);
+	}
+
+	errno = saved_errno;
+}
+
+void
+log_warnx(const char *emsg, ...)
+{
+	va_list ap;
+
+	va_start(ap, emsg);
+	vlog(LOG_CRIT, emsg, ap);
+	va_end(ap);
+}
+
+void
+log_info(const char *emsg, ...)
+{
+	va_list ap;
+
+	va_start(ap, emsg);
+	vlog(LOG_INFO, emsg, ap);
+	va_end(ap);
+}
+
+void
+log_debug(const char *emsg, ...)
+{
+	va_list ap;
+
+	if (verbose > 1) {
+		va_start(ap, emsg);
+		vlog(LOG_DEBUG, emsg, ap);
+		va_end(ap);
+	}
+}
+
+static void
+vfatalc(int code, const char *emsg, va_list ap)
+{
+	static char s[BUFSIZ];
+	const char *sep;
+
+	if (emsg != NULL) {
+		(void)vsnprintf(s, sizeof(s), emsg, ap);
+		sep = ": ";
+	} else {
+		s[0] = '\0';
+		sep = "";
+	}
+	if (code)
+		logit(LOG_CRIT, "%s: %s%s%s",
+		    log_procname, s, sep, strerror(code));
+	else
+		logit(LOG_CRIT, "%s%s%s", log_procname, sep, s);
+}
+
+void
+fatal(const char *emsg, ...)
+{
+	va_list ap;
+
+	va_start(ap, emsg);
+	vfatalc(errno, emsg, ap);
+	va_end(ap);
+	exit(1);
+}
+
+void
+fatalx(const char *emsg, ...)
+{
+	va_list ap;
+
+	va_start(ap, emsg);
+	vfatalc(0, emsg, ap);
+	va_end(ap);
+	exit(1);
+}
blob - /dev/null
blob + c7b300955ba979db3cd4d8308802a8179f9d7ac4 (mode 644)
--- /dev/null
+++ gotwebd/parse.y
@@ -0,0 +1,1339 @@
+/*
+ * Copyright (c) 2016-2019, 2020-2021 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2004, 2005 Esben Norby <norby@openbsd.org>
+ * Copyright (c) 2004 Ryan McBride <mcbride@openbsd.org>
+ * Copyright (c) 2002, 2003, 2004 Henning Brauer <henning@openbsd.org>
+ * Copyright (c) 2001 Markus Friedl.  All rights reserved.
+ * Copyright (c) 2001 Daniel Hartmeier.  All rights reserved.
+ * Copyright (c) 2001 Theo de Raadt.  All rights reserved.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+%{
+#include <sys/ioctl.h>
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include <arpa/inet.h>
+
+#include <ctype.h>
+#include <err.h>
+#include <errno.h>
+#include <event.h>
+#include <ifaddrs.h>
+#include <imsg.h>
+#include <limits.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <syslog.h>
+#include <tls.h>
+#include <unistd.h>
+
+#include "proc.h"
+#include "gotwebd.h"
+
+TAILQ_HEAD(files, file)		 files = TAILQ_HEAD_INITIALIZER(files);
+static struct file {
+	TAILQ_ENTRY(file)	 entry;
+	FILE			*stream;
+	char			*name;
+	int			 lineno;
+	int			 errors;
+} *file;
+struct file	*newfile(const char *, int);
+static void	 closefile(struct file *);
+int		 check_file_secrecy(int, const char *);
+int		 yyparse(void);
+int		 yylex(void);
+int		 yyerror(const char *, ...)
+    __attribute__((__format__ (printf, 1, 2)))
+    __attribute__((__nonnull__ (1)));
+int		 kw_cmp(const void *, const void *);
+int		 lookup(char *);
+int		 lgetc(int);
+int		 lungetc(int);
+int		 findeol(void);
+
+TAILQ_HEAD(symhead, sym)	 symhead = TAILQ_HEAD_INITIALIZER(symhead);
+struct sym {
+	TAILQ_ENTRY(sym)	 entry;
+	int			 used;
+	int			 persist;
+	char			*nam;
+	char			*val;
+};
+
+int	 symset(const char *, const char *, int);
+char	*symget(const char *);
+
+static int		 errors;
+
+static struct gotwebd		*gotwebd;
+static struct server		*new_srv;
+static struct server		*conf_new_server(const char *);
+int				 getservice(const char *);
+int				 n;
+
+int		 get_addrs(const char *, struct addresslist *, in_port_t);
+struct address	*host_v4(const char *);
+struct address	*host_v6(const char *);
+int		 host_dns(const char *, struct addresslist *,
+		    int, in_port_t, const char *, int);
+int		 host_if(const char *, struct addresslist *,
+		    int, in_port_t, const char *, int);
+int		 host(const char *, struct addresslist *,
+		    int, in_port_t, const char *, int);
+int		 is_if_in_group(const char *, const char *);
+
+typedef struct {
+	union {
+		long long	 number;
+		char		*string;
+		in_port_t	 port;
+	} v;
+	int lineno;
+} YYSTYPE;
+
+%}
+
+%token	BIND INTERFACE WWW_PATH MAX_REPOS SITE_NAME SITE_OWNER SITE_LINK LOGO
+%token	LOGO_URL SHOW_REPO_OWNER SHOW_REPO_AGE SHOW_REPO_DESCRIPTION
+%token	MAX_REPOS_DISPLAY REPOS_PATH MAX_COMMITS_DISPLAY ON ERROR
+%token	SHOW_SITE_OWNER SHOW_REPO_CLONEURL PORT PREFORK FCGI_SOCKET
+%token	UNIX_SOCKET UNIX_SOCKET_NAME SERVER CHROOT CUSTOM_CSS
+
+%token	<v.string>	STRING
+%type	<v.port>	fcgiport
+%token	<v.number>	NUMBER
+%type	<v.number>	boolean
+
+%%
+
+grammar		:
+		| grammar '\n'
+		| grammar main '\n'
+		| grammar server '\n'
+		;
+
+boolean		: STRING {
+			if (strcasecmp($1, "1") == 0 ||
+			    strcasecmp($1, "yes") == 0 ||
+			    strcasecmp($1, "on") == 0)
+				$$ = 1;
+			else if (strcasecmp($1, "0") == 0 ||
+			    strcasecmp($1, "off") == 0 ||
+			    strcasecmp($1, "no") == 0)
+				$$ = 0;
+			else {
+				yyerror("invalid boolean value '%s'", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+		}
+		| ON { $$ = 1; }
+		| NUMBER { $$ = $1; }
+		;
+
+fcgiport	: NUMBER {
+			if ($1 <= 0 || $1 > (int)USHRT_MAX) {
+				yyerror("invalid port: %lld", $1);
+				YYERROR;
+			}
+			$$ = htons($1);
+		}
+		| STRING {
+			int	 val;
+
+			if ((val = getservice($1)) == -1) {
+				yyerror("invalid port: %s", $1);
+				free($1);
+				YYERROR;
+			}
+			free($1);
+
+			$$ = val;
+		}
+		;
+
+main		: PREFORK NUMBER {
+			gotwebd->prefork_gotwebd = $2;
+		}
+		| CHROOT STRING {
+			n = strlcpy(gotwebd->httpd_chroot, $2,
+			    sizeof(gotwebd->httpd_chroot));
+			if (n >= sizeof(gotwebd->httpd_chroot)) {
+				yyerror("%s: httpd_chroot truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| FCGI_SOCKET boolean {
+			gotwebd->fcgi_socket = $2;
+		}
+		| FCGI_SOCKET boolean  {
+			gotwebd->fcgi_socket = $2;
+		} '{' optnl socketopts4 '}'
+		| UNIX_SOCKET boolean {
+			gotwebd->unix_socket = $2;
+		}
+		| UNIX_SOCKET_NAME STRING {
+			n = snprintf(gotwebd->unix_socket_name,
+			    sizeof(gotwebd->unix_socket_name), "%s%s",
+			    strlen(gotwebd->httpd_chroot) ?
+			    gotwebd->httpd_chroot : D_HTTPD_CHROOT, $2);
+			if (n < 0) {
+				yyerror("%s: unix_socket_name truncated",
+				    __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		;
+
+server		: SERVER STRING {
+			struct server *srv;
+
+			TAILQ_FOREACH(srv, gotwebd->servers, entry) {
+				if (strcmp(srv->name, $2) == 0) {
+					yyerror("server name exists '%s'", $2);
+					free($2);
+					YYERROR;
+				}
+			}
+
+			new_srv = conf_new_server($2);
+			if (new_srv->fcgi_socket)
+				if (get_addrs(new_srv->fcgi_socket_bind,
+				    new_srv->al,
+				    new_srv->fcgi_socket_port) == -1) {
+					yyerror("could not get tcp iface "
+					    "addrs");
+					YYERROR;
+				}
+			log_debug("adding server %s", $2);
+			free($2);
+		}
+		| SERVER STRING {
+			struct server *srv;
+
+			TAILQ_FOREACH(srv, gotwebd->servers, entry) {
+				if (strcmp(srv->name, $2) == 0) {
+					yyerror("server name exists '%s'", $2);
+					free($2);
+					YYERROR;
+				}
+			}
+
+			new_srv = conf_new_server($2);
+			log_debug("adding server %s", $2);
+			free($2);
+		} '{' optnl serveropts2 '}' {
+			if (get_addrs(new_srv->fcgi_socket_bind,
+			    new_srv->al, new_srv->fcgi_socket_port) == -1) {
+				yyerror("could not get tcp iface addrs");
+				YYERROR;
+			}
+		}
+		;
+
+serveropts1	: REPOS_PATH STRING {
+			n = strlcpy(new_srv->repos_path, $2,
+			    sizeof(new_srv->repos_path));
+			if (n >= sizeof(new_srv->repos_path)) {
+				yyerror("%s: repos_path truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| SITE_NAME STRING {
+			n = strlcpy(new_srv->site_name, $2,
+			    sizeof(new_srv->site_name));
+			if (n >= sizeof(new_srv->site_name)) {
+				yyerror("%s: site_name truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| SITE_OWNER STRING {
+			n = strlcpy(new_srv->site_owner, $2,
+			    sizeof(new_srv->site_owner));
+			if (n >= sizeof(new_srv->site_owner)) {
+				yyerror("%s: site_owner truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| SITE_LINK STRING {
+			n = strlcpy(new_srv->site_link, $2,
+			    sizeof(new_srv->site_link));
+			if (n >= sizeof(new_srv->site_link)) {
+				yyerror("%s: site_link truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| LOGO STRING {
+			n = strlcpy(new_srv->logo, $2, sizeof(new_srv->logo));
+			if (n >= sizeof(new_srv->logo)) {
+				yyerror("%s: logo truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| LOGO_URL STRING {
+			n = strlcpy(new_srv->logo_url, $2,
+			    sizeof(new_srv->logo_url));
+			if (n >= sizeof(new_srv->logo_url)) {
+				yyerror("%s: logo_url truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| CUSTOM_CSS STRING {
+			n = strlcpy(new_srv->custom_css, $2,
+			    sizeof(new_srv->custom_css));
+			if (n >= sizeof(new_srv->custom_css)) {
+				yyerror("%s: custom_css truncated", __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		| MAX_REPOS NUMBER {
+			if ($2 > 0)
+				new_srv->max_repos = $2;
+		}
+		| SHOW_SITE_OWNER boolean {
+			new_srv->show_site_owner = $2;
+		}
+		| SHOW_REPO_OWNER boolean {
+			new_srv->show_repo_owner = $2;
+		}
+		| SHOW_REPO_AGE boolean {
+			new_srv->show_repo_age = $2;
+		}
+		| SHOW_REPO_DESCRIPTION boolean {
+			new_srv->show_repo_description = $2;
+		}
+		| SHOW_REPO_CLONEURL boolean {
+			new_srv->show_repo_cloneurl = $2;
+		}
+		| MAX_REPOS_DISPLAY NUMBER {
+				new_srv->max_repos_display = $2;
+		}
+		| MAX_COMMITS_DISPLAY NUMBER {
+			if ($2 > 0)
+				new_srv->max_commits_display = $2;
+		}
+		| FCGI_SOCKET boolean {
+			new_srv->fcgi_socket = $2;
+		}
+		| FCGI_SOCKET boolean  {
+			new_srv->fcgi_socket = $2;
+		} '{' optnl socketopts2 '}'
+		| UNIX_SOCKET boolean {
+			new_srv->unix_socket = $2;
+		}
+		| UNIX_SOCKET_NAME STRING {
+			n = snprintf(new_srv->unix_socket_name,
+			    sizeof(new_srv->unix_socket_name), "%s%s",
+			    strlen(gotwebd->httpd_chroot) ?
+			    gotwebd->httpd_chroot : D_HTTPD_CHROOT, $2);
+			if (n < 0) {
+				yyerror("%s: unix_socket_name truncated",
+				    __func__);
+				free($2);
+				YYERROR;
+			}
+			free($2);
+		}
+		;
+
+serveropts2	: serveropts2 serveropts1 nl
+		| serveropts1 optnl
+		;
+
+socketopts1	: BIND INTERFACE STRING {
+			n = strlcpy(new_srv->fcgi_socket_bind, $3,
+			    sizeof(new_srv->fcgi_socket_bind));
+			if (n >= sizeof(new_srv->fcgi_socket_bind)) {
+				yyerror("%s: fcgi_socket_bind truncated",
+				    __func__);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		| PORT fcgiport {
+			struct server	*srv;
+
+			TAILQ_FOREACH(srv, gotwebd->servers, entry) {
+				if (srv->fcgi_socket_port == $2) {
+					yyerror("port already assigned");
+					YYERROR;
+				}
+			}
+			new_srv->fcgi_socket_port = $2;
+		}
+		;
+
+socketopts2	: socketopts2 socketopts1 nl
+		| socketopts1 optnl
+		;
+
+socketopts3	: BIND INTERFACE STRING {
+			n = strlcpy(gotwebd->fcgi_socket_bind, $3,
+			    sizeof(gotwebd->fcgi_socket_bind));
+			if (n >= sizeof(gotwebd->fcgi_socket_bind)) {
+				yyerror("%s: fcgi_socket_bind truncated",
+				    __func__);
+				free($3);
+				YYERROR;
+			}
+			free($3);
+		}
+		| PORT fcgiport {
+			gotwebd->fcgi_socket_port = $2;
+		}
+		;
+
+socketopts4	: socketopts4 socketopts3 nl
+		| socketopts3 optnl
+		;
+
+nl		: '\n' optnl
+		;
+
+optnl		: '\n' optnl		/* zero or more newlines */
+		| /* empty */
+		;
+
+%%
+
+struct keywords {
+	const char	*k_name;
+	int		 k_val;
+};
+
+int
+yyerror(const char *fmt, ...)
+{
+	va_list ap;
+	char *msg;
+
+	file->errors++;
+	va_start(ap, fmt);
+	if (vasprintf(&msg, fmt, ap) == -1)
+		fatalx("yyerror vasprintf");
+	va_end(ap);
+	logit(LOG_CRIT, "%s:%d: %s", file->name, yylval.lineno, msg);
+	free(msg);
+	return (0);
+}
+
+int
+kw_cmp(const void *k, const void *e)
+{
+	return (strcmp(k, ((const struct keywords *)e)->k_name));
+}
+
+int
+lookup(char *s)
+{
+	/* This has to be sorted always. */
+	static const struct keywords keywords[] = {
+		{ "bind",			BIND },
+		{ "chroot",			CHROOT },
+		{ "custom_css",			CUSTOM_CSS },
+		{ "fcgi_socket",		FCGI_SOCKET },
+		{ "interface",			INTERFACE },
+		{ "logo",			LOGO },
+		{ "logo_url"	,		LOGO_URL },
+		{ "max_commits_display",	MAX_COMMITS_DISPLAY },
+		{ "max_repos",			MAX_REPOS },
+		{ "max_repos_display",		MAX_REPOS_DISPLAY },
+		{ "port",			PORT },
+		{ "prefork",			PREFORK },
+		{ "repos_path",			REPOS_PATH },
+		{ "server",			SERVER },
+		{ "show_repo_age",		SHOW_REPO_AGE },
+		{ "show_repo_cloneurl",		SHOW_REPO_CLONEURL },
+		{ "show_repo_description",	SHOW_REPO_DESCRIPTION },
+		{ "show_repo_owner",		SHOW_REPO_OWNER },
+		{ "show_site_owner",		SHOW_SITE_OWNER },
+		{ "site_link",			SITE_LINK },
+		{ "site_name",			SITE_NAME },
+		{ "site_owner",			SITE_OWNER },
+		{ "unix_socket",		UNIX_SOCKET },
+		{ "unix_socket_name",		UNIX_SOCKET_NAME },
+	};
+	const struct keywords *p;
+
+	p = bsearch(s, keywords, sizeof(keywords)/sizeof(keywords[0]),
+	    sizeof(keywords[0]), kw_cmp);
+
+	if (p)
+		return (p->k_val);
+	else
+		return (STRING);
+}
+
+#define MAXPUSHBACK	128
+
+unsigned char *parsebuf;
+int parseindex;
+unsigned char pushback_buffer[MAXPUSHBACK];
+int pushback_index = 0;
+
+int
+lgetc(int quotec)
+{
+	int c, next;
+
+	if (parsebuf) {
+		/* Read character from the parsebuffer instead of input. */
+		if (parseindex >= 0) {
+			c = parsebuf[parseindex++];
+			if (c != '\0')
+				return (c);
+			parsebuf = NULL;
+		} else
+			parseindex++;
+	}
+
+	if (pushback_index)
+		return (pushback_buffer[--pushback_index]);
+
+	if (quotec) {
+		c = getc(file->stream);
+		if (c == EOF)
+			yyerror("reached end of file while parsing "
+			    "quoted string");
+		return (c);
+	}
+
+	c = getc(file->stream);
+	while (c == '\\') {
+		next = getc(file->stream);
+		if (next != '\n') {
+			c = next;
+			break;
+		}
+		yylval.lineno = file->lineno;
+		file->lineno++;
+		c = getc(file->stream);
+	}
+
+	return (c);
+}
+
+int
+lungetc(int c)
+{
+	if (c == EOF)
+		return (EOF);
+	if (parsebuf) {
+		parseindex--;
+		if (parseindex >= 0)
+			return (c);
+	}
+	if (pushback_index < MAXPUSHBACK-1)
+		return (pushback_buffer[pushback_index++] = c);
+	else
+		return (EOF);
+}
+
+int
+findeol(void)
+{
+	int c;
+
+	parsebuf = NULL;
+
+	/* Skip to either EOF or the first real EOL. */
+	while (1) {
+		if (pushback_index)
+			c = pushback_buffer[--pushback_index];
+		else
+			c = lgetc(0);
+		if (c == '\n') {
+			file->lineno++;
+			break;
+		}
+		if (c == EOF)
+			break;
+	}
+	return (ERROR);
+}
+
+int
+yylex(void)
+{
+	unsigned char buf[8096];
+	unsigned char *p, *val;
+	int quotec, next, c;
+	int token;
+
+top:
+	p = buf;
+	c = lgetc(0);
+	while (c == ' ' || c == '\t')
+		c = lgetc(0); /* nothing */
+
+	yylval.lineno = file->lineno;
+	if (c == '#') {
+		c = lgetc(0);
+		while (c != '\n' && c != EOF)
+			c = lgetc(0); /* nothing */
+	}
+	if (c == '$' && parsebuf == NULL) {
+		while (1) {
+			c = lgetc(0);
+			if (c == EOF)
+				return (0);
+
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			if (isalnum(c) || c == '_') {
+				*p++ = c;
+				continue;
+			}
+			*p = '\0';
+			lungetc(c);
+			break;
+		}
+		val = symget(buf);
+		if (val == NULL) {
+			yyerror("macro '%s' not defined", buf);
+			return (findeol());
+		}
+		parsebuf = val;
+		parseindex = 0;
+		goto top;
+	}
+
+	switch (c) {
+	case '\'':
+	case '"':
+		quotec = c;
+		while (1) {
+			c = lgetc(quotec);
+			if (c == EOF)
+				return (0);
+			if (c == '\n') {
+				file->lineno++;
+				continue;
+			} else if (c == '\\') {
+				next = lgetc(quotec);
+				if (next == EOF)
+					return (0);
+				if (next == quotec || c == ' ' || c == '\t')
+					c = next;
+				else if (next == '\n') {
+					file->lineno++;
+					continue;
+				} else
+					lungetc(next);
+			} else if (c == quotec) {
+				*p = '\0';
+				break;
+			} else if (c == '\0') {
+				yyerror("syntax error");
+				return (findeol());
+			}
+			if (p + 1 >= buf + sizeof(buf) - 1) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			*p++ = c;
+		}
+		yylval.v.string = strdup(buf);
+		if (yylval.v.string == NULL)
+			err(1, "yylex: strdup");
+		return (STRING);
+	}
+
+#define allowed_to_end_number(x) \
+	(isspace(x) || x == ')' || x ==',' || x == '/' || x == '}' || x == '=')
+
+	if (c == '-' || isdigit(c)) {
+		do {
+			*p++ = c;
+			if ((unsigned)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			c = lgetc(0);
+		} while (c != EOF && isdigit(c));
+		lungetc(c);
+		if (p == buf + 1 && buf[0] == '-')
+			goto nodigits;
+		if (c == EOF || allowed_to_end_number(c)) {
+			const char *errstr = NULL;
+
+			*p = '\0';
+			yylval.v.number = strtonum(buf, LLONG_MIN,
+			    LLONG_MAX, &errstr);
+			if (errstr) {
+				yyerror("\"%s\" invalid number: %s",
+				    buf, errstr);
+				return (findeol());
+			}
+			return (NUMBER);
+		} else {
+nodigits:
+			while (p > buf + 1)
+				lungetc(*--p);
+			c = *--p;
+			if (c == '-')
+				return (c);
+		}
+	}
+
+#define allowed_in_string(x) \
+	(isalnum(x) || (ispunct(x) && x != '(' && x != ')' && \
+	x != '{' && x != '}' && \
+	x != '!' && x != '=' && x != '#' && \
+	x != ','))
+
+	if (isalnum(c) || c == ':' || c == '_') {
+		do {
+			*p++ = c;
+			if ((unsigned)(p-buf) >= sizeof(buf)) {
+				yyerror("string too long");
+				return (findeol());
+			}
+			c = lgetc(0);
+		} while (c != EOF && (allowed_in_string(c)));
+		lungetc(c);
+		*p = '\0';
+		token = lookup(buf);
+		if (token == STRING) {
+			yylval.v.string = strdup(buf);
+			if (yylval.v.string == NULL)
+				err(1, "yylex: strdup");
+		}
+		return (token);
+	}
+	if (c == '\n') {
+		yylval.lineno = file->lineno;
+		file->lineno++;
+	}
+	if (c == EOF)
+		return (0);
+	return (c);
+}
+
+int
+check_file_secrecy(int fd, const char *fname)
+{
+	struct stat st;
+
+	if (fstat(fd, &st)) {
+		log_warn("cannot stat %s", fname);
+		return (-1);
+	}
+	if (st.st_uid != 0 && st.st_uid != getuid()) {
+		log_warnx("%s: owner not root or current user", fname);
+		return (-1);
+	}
+	if (st.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO)) {
+		log_warnx("%s: group writable or world read/writable", fname);
+		return (-1);
+	}
+	return (0);
+}
+
+struct file *
+newfile(const char *name, int secret)
+{
+	struct file *nfile;
+
+	nfile = calloc(1, sizeof(struct file));
+	if (nfile == NULL) {
+		log_warn("calloc");
+		return (NULL);
+	}
+	nfile->name = strdup(name);
+	if (nfile->name == NULL) {
+		log_warn("strdup");
+		free(nfile);
+		return (NULL);
+	}
+	nfile->stream = fopen(nfile->name, "r");
+	if (nfile->stream == NULL) {
+		/* no warning, we don't require a conf file */
+		free(nfile->name);
+		free(nfile);
+		return (NULL);
+	} else if (secret &&
+	    check_file_secrecy(fileno(nfile->stream), nfile->name)) {
+		fclose(nfile->stream);
+		free(nfile->name);
+		free(nfile);
+		return (NULL);
+	}
+	nfile->lineno = 1;
+	return (nfile);
+}
+
+static void
+closefile(struct file *xfile)
+{
+	fclose(xfile->stream);
+	free(xfile->name);
+	free(xfile);
+}
+
+int
+parse_config(const char *filename, struct gotwebd *env)
+{
+	struct sym *sym, *next;
+
+	file = newfile(filename, 0);
+	if (file == NULL)
+		/* just return, as we don't require a conf file */
+		return (0);
+
+	if (config_init(env) == -1)
+		fatalx("failed to initialize configuration");
+
+	gotwebd = env;
+
+	yyparse();
+	errors = file->errors;
+	closefile(file);
+
+	/* Free macros and check which have not been used. */
+	TAILQ_FOREACH_SAFE(sym, &symhead, entry, next) {
+		if ((gotwebd->gotwebd_verbose > 1) && !sym->used)
+			fprintf(stderr, "warning: macro '%s' not used\n",
+			    sym->nam);
+		if (!sym->persist) {
+			free(sym->nam);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+
+	if (errors)
+		return (-1);
+
+	/* just add default server if no config specified */
+	if (gotwebd->server_cnt == 0) {
+		new_srv = conf_new_server(D_SITENAME);
+		log_debug("%s: adding default server %s", __func__, D_SITENAME);
+	}
+
+	/* setup our listening sockets */
+	sockets_parse_sockets(env);
+
+	return (0);
+}
+
+struct server *
+conf_new_server(const char *name)
+{
+	struct server *srv = NULL;
+	int val;
+
+	srv = calloc(1, sizeof(*srv));
+	if (srv == NULL)
+		fatalx("%s: calloc", __func__);
+
+	n = strlcpy(srv->name, name, sizeof(srv->name));
+	if (n >= sizeof(srv->name))
+		fatalx("%s: strlcpy", __func__);
+	n = snprintf(srv->unix_socket_name,
+	    sizeof(srv->unix_socket_name), "%s%s", D_HTTPD_CHROOT,
+	    D_UNIX_SOCKET);
+	if (n < 0)
+		fatalx("%s: snprintf", __func__);
+	n = strlcpy(srv->repos_path, D_GOTPATH,
+	    sizeof(srv->repos_path));
+	if (n >= sizeof(srv->repos_path))
+		fatalx("%s: strlcpy", __func__);
+	n = strlcpy(srv->site_name, D_SITENAME,
+	    sizeof(srv->site_name));
+	if (n >= sizeof(srv->site_name))
+		fatalx("%s: strlcpy", __func__);
+	n = strlcpy(srv->site_owner, D_SITEOWNER,
+	    sizeof(srv->site_owner));
+	if (n >= sizeof(srv->site_owner))
+		fatalx("%s: strlcpy", __func__);
+	n = strlcpy(srv->site_link, D_SITELINK,
+	    sizeof(srv->site_link));
+	if (n >= sizeof(srv->site_link))
+		fatalx("%s: strlcpy", __func__);
+	n = strlcpy(srv->logo, D_GOTLOGO,
+	    sizeof(srv->logo));
+	if (n >= sizeof(srv->logo))
+		fatalx("%s: strlcpy", __func__);
+	n = strlcpy(srv->logo_url, D_GOTURL, sizeof(srv->logo_url));
+	if (n >= sizeof(srv->logo_url))
+		fatalx("%s: strlcpy", __func__);
+	n = strlcpy(srv->custom_css, D_GOTWEBCSS, sizeof(srv->custom_css));
+	if (n >= sizeof(srv->custom_css))
+		fatalx("%s: strlcpy", __func__);
+
+	val = getservice(D_FCGI_PORT);
+	srv->fcgi_socket_port = gotwebd->fcgi_socket_port ?
+	    gotwebd->fcgi_socket_port: htons(val);
+
+	srv->show_site_owner = D_SHOWSOWNER;
+	srv->show_repo_owner = D_SHOWROWNER;
+	srv->show_repo_age = D_SHOWAGE;
+	srv->show_repo_description = D_SHOWDESC;
+	srv->show_repo_cloneurl = D_SHOWURL;
+
+	srv->max_repos_display = D_MAXREPODISP;
+	srv->max_commits_display = D_MAXCOMMITDISP;
+	srv->max_repos = D_MAXREPO;
+
+	srv->unix_socket = 1;
+	srv->fcgi_socket = gotwebd->fcgi_socket ? gotwebd->fcgi_socket : 0;
+
+	if ((srv->al = calloc(1, sizeof(*srv->al))) == NULL)
+		fatalx("%s: calloc", __func__);
+
+	TAILQ_INIT(srv->al);
+	TAILQ_INSERT_TAIL(gotwebd->servers, srv, entry);
+	gotwebd->server_cnt++;
+
+	return srv;
+};
+
+int
+symset(const char *nam, const char *val, int persist)
+{
+	struct sym *sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->nam) == 0)
+			break;
+	}
+
+	if (sym != NULL) {
+		if (sym->persist == 1)
+			return (0);
+		else {
+			free(sym->nam);
+			free(sym->val);
+			TAILQ_REMOVE(&symhead, sym, entry);
+			free(sym);
+		}
+	}
+	sym = calloc(1, sizeof(*sym));
+	if (sym == NULL)
+		return (-1);
+
+	sym->nam = strdup(nam);
+	if (sym->nam == NULL) {
+		free(sym);
+		return (-1);
+	}
+	sym->val = strdup(val);
+	if (sym->val == NULL) {
+		free(sym->nam);
+		free(sym);
+		return (-1);
+	}
+	sym->used = 0;
+	sym->persist = persist;
+	TAILQ_INSERT_TAIL(&symhead, sym, entry);
+	return (0);
+}
+
+int
+cmdline_symset(char *s)
+{
+	char *sym, *val;
+	int ret;
+	size_t len;
+
+	val = strrchr(s, '=');
+	if (val == NULL)
+		return (-1);
+
+	len = strlen(s) - strlen(val) + 1;
+	sym = malloc(len);
+	if (sym == NULL)
+		fatal("%s: malloc", __func__);
+
+	memcpy(&sym, s, len);
+
+	ret = symset(sym, val + 1, 1);
+	free(sym);
+
+	return (ret);
+}
+
+char *
+symget(const char *nam)
+{
+	struct sym *sym;
+
+	TAILQ_FOREACH(sym, &symhead, entry) {
+		if (strcmp(nam, sym->nam) == 0) {
+			sym->used = 1;
+			return (sym->val);
+		}
+	}
+	return (NULL);
+}
+
+int
+getservice(const char *n)
+{
+	struct servent *s;
+	const char *errstr;
+	long long llval;
+
+	llval = strtonum(n, 0, UINT16_MAX, &errstr);
+	if (errstr) {
+		s = getservbyname(n, "tcp");
+		if (s == NULL)
+			s = getservbyname(n, "udp");
+		if (s == NULL)
+			return (-1);
+		return (s->s_port);
+	}
+
+	return (htons((unsigned short)llval));
+}
+
+struct address *
+host_v4(const char *s)
+{
+	struct in_addr ina;
+	struct sockaddr_in *sain;
+	struct address *h;
+
+	memset(&ina, 0, sizeof(ina));
+	if (inet_pton(AF_INET, s, &ina) != 1)
+		return (NULL);
+
+	if ((h = calloc(1, sizeof(*h))) == NULL)
+		fatal(__func__);
+	sain = (struct sockaddr_in *)&h->ss;
+	sain->sin_len = sizeof(struct sockaddr_in);
+	sain->sin_family = AF_INET;
+	sain->sin_addr.s_addr = ina.s_addr;
+	if (sain->sin_addr.s_addr == INADDR_ANY)
+		h->prefixlen = 0; /* 0.0.0.0 address */
+	else
+		h->prefixlen = -1; /* host address */
+	return (h);
+}
+
+struct address *
+host_v6(const char *s)
+{
+	struct addrinfo hints, *res;
+	struct sockaddr_in6 *sa_in6;
+	struct address *h = NULL;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_INET6;
+	hints.ai_socktype = SOCK_DGRAM; /* dummy */
+	hints.ai_flags = AI_NUMERICHOST;
+	if (getaddrinfo(s, "0", &hints, &res) == 0) {
+		if ((h = calloc(1, sizeof(*h))) == NULL)
+			fatal(__func__);
+		sa_in6 = (struct sockaddr_in6 *)&h->ss;
+		sa_in6->sin6_len = sizeof(struct sockaddr_in6);
+		sa_in6->sin6_family = AF_INET6;
+		memcpy(&sa_in6->sin6_addr,
+		    &((struct sockaddr_in6 *)res->ai_addr)->sin6_addr,
+		    sizeof(sa_in6->sin6_addr));
+		sa_in6->sin6_scope_id =
+		    ((struct sockaddr_in6 *)res->ai_addr)->sin6_scope_id;
+		if (memcmp(&sa_in6->sin6_addr, &in6addr_any,
+		    sizeof(sa_in6->sin6_addr)) == 0)
+			h->prefixlen = 0; /* any address */
+		else
+			h->prefixlen = -1; /* host address */
+		freeaddrinfo(res);
+	}
+
+	return (h);
+}
+
+int
+host_dns(const char *s, struct addresslist *al, int max,
+    in_port_t port, const char *ifname, int ipproto)
+{
+	struct addrinfo hints, *res0, *res;
+	int error, cnt = 0;
+	struct sockaddr_in *sain;
+	struct sockaddr_in6 *sin6;
+	struct address *h;
+
+	if ((cnt = host_if(s, al, max, port, ifname, ipproto)) != 0)
+		return (cnt);
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = PF_UNSPEC;
+	hints.ai_socktype = SOCK_DGRAM; /* DUMMY */
+	hints.ai_flags = AI_ADDRCONFIG;
+	error = getaddrinfo(s, NULL, &hints, &res0);
+	if (error == EAI_AGAIN || error == EAI_NODATA || error == EAI_NONAME)
+		return (0);
+	if (error) {
+		log_warnx("%s: could not parse \"%s\": %s", __func__, s,
+		    gai_strerror(error));
+		return (-1);
+	}
+
+	for (res = res0; res && cnt < max; res = res->ai_next) {
+		if (res->ai_family != AF_INET &&
+		    res->ai_family != AF_INET6)
+			continue;
+		if ((h = calloc(1, sizeof(*h))) == NULL)
+			fatal(__func__);
+
+		if (port)
+			h->port = port;
+		if (ifname != NULL) {
+			if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >=
+			    sizeof(h->ifname)) {
+				log_warnx("%s: interface name truncated",
+				    __func__);
+				freeaddrinfo(res0);
+				free(h);
+				return (-1);
+			}
+		}
+		if (ipproto != -1)
+			h->ipproto = ipproto;
+		h->ss.ss_family = res->ai_family;
+		h->prefixlen = -1; /* host address */
+
+		if (res->ai_family == AF_INET) {
+			sain = (struct sockaddr_in *)&h->ss;
+			sain->sin_len = sizeof(struct sockaddr_in);
+			sain->sin_addr.s_addr = ((struct sockaddr_in *)
+			    res->ai_addr)->sin_addr.s_addr;
+		} else {
+			sin6 = (struct sockaddr_in6 *)&h->ss;
+			sin6->sin6_len = sizeof(struct sockaddr_in6);
+			memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *)
+			    res->ai_addr)->sin6_addr, sizeof(struct in6_addr));
+		}
+
+		TAILQ_INSERT_HEAD(al, h, entry);
+		cnt++;
+	}
+	if (cnt == max && res) {
+		log_warnx("%s: %s resolves to more than %d hosts", __func__,
+		    s, max);
+	}
+	freeaddrinfo(res0);
+	return (cnt);
+}
+
+int
+host_if(const char *s, struct addresslist *al, int max,
+    in_port_t port, const char *ifname, int ipproto)
+{
+	struct ifaddrs *ifap, *p;
+	struct sockaddr_in *sain;
+	struct sockaddr_in6 *sin6;
+	struct address *h;
+	int cnt = 0, af;
+
+	if (getifaddrs(&ifap) == -1)
+		fatal("getifaddrs");
+
+	/* First search for IPv4 addresses */
+	af = AF_INET;
+
+ nextaf:
+	for (p = ifap; p != NULL && cnt < max; p = p->ifa_next) {
+		if (p->ifa_addr == NULL ||
+		    p->ifa_addr->sa_family != af ||
+		    (strcmp(s, p->ifa_name) != 0 &&
+		    !is_if_in_group(p->ifa_name, s)))
+			continue;
+		if ((h = calloc(1, sizeof(*h))) == NULL)
+			fatal("calloc");
+
+		if (port)
+			h->port = port;
+		if (ifname != NULL) {
+			if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >=
+			    sizeof(h->ifname)) {
+				log_warnx("%s: interface name truncated",
+				    __func__);
+				free(h);
+				freeifaddrs(ifap);
+				return (-1);
+			}
+		}
+		if (ipproto != -1)
+			h->ipproto = ipproto;
+		h->ss.ss_family = af;
+		h->prefixlen = -1; /* host address */
+
+		if (af == AF_INET) {
+			sain = (struct sockaddr_in *)&h->ss;
+			sain->sin_len = sizeof(struct sockaddr_in);
+			sain->sin_addr.s_addr = ((struct sockaddr_in *)
+			    p->ifa_addr)->sin_addr.s_addr;
+		} else {
+			sin6 = (struct sockaddr_in6 *)&h->ss;
+			sin6->sin6_len = sizeof(struct sockaddr_in6);
+			memcpy(&sin6->sin6_addr, &((struct sockaddr_in6 *)
+			    p->ifa_addr)->sin6_addr, sizeof(struct in6_addr));
+			sin6->sin6_scope_id = ((struct sockaddr_in6 *)
+			    p->ifa_addr)->sin6_scope_id;
+		}
+
+		TAILQ_INSERT_HEAD(al, h, entry);
+		cnt++;
+	}
+	if (af == AF_INET) {
+		/* Next search for IPv6 addresses */
+		af = AF_INET6;
+		goto nextaf;
+	}
+
+	if (cnt > max) {
+		log_warnx("%s: %s resolves to more than %d hosts", __func__,
+		    s, max);
+	}
+	freeifaddrs(ifap);
+	return (cnt);
+}
+
+int
+host(const char *s, struct addresslist *al, int max,
+    in_port_t port, const char *ifname, int ipproto)
+{
+	struct address *h;
+
+	h = host_v4(s);
+
+	/* IPv6 address? */
+	if (h == NULL)
+		h = host_v6(s);
+
+	if (h != NULL) {
+		if (port)
+			h->port = port;
+		if (ifname != NULL) {
+			if (strlcpy(h->ifname, ifname, sizeof(h->ifname)) >=
+			    sizeof(h->ifname)) {
+				log_warnx("%s: interface name truncated",
+				    __func__);
+				free(h);
+				return (-1);
+			}
+		}
+		if (ipproto != -1)
+			h->ipproto = ipproto;
+
+		TAILQ_INSERT_HEAD(al, h, entry);
+		return (1);
+	}
+
+	return (host_dns(s, al, max, port, ifname, ipproto));
+}
+
+int
+is_if_in_group(const char *ifname, const char *groupname)
+{
+	unsigned int len;
+	struct ifgroupreq ifgr;
+	struct ifg_req *ifg;
+	int s;
+	int ret = 0;
+
+	if ((s = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
+		err(1, "socket");
+
+	memset(&ifgr, 0, sizeof(ifgr));
+	if (strlcpy(ifgr.ifgr_name, ifname, IFNAMSIZ) >= IFNAMSIZ)
+		err(1, "IFNAMSIZ");
+	if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1) {
+		if (errno == EINVAL || errno == ENOTTY)
+			goto end;
+		err(1, "SIOCGIFGROUP");
+	}
+
+	len = ifgr.ifgr_len;
+	ifgr.ifgr_groups = calloc(len / sizeof(struct ifg_req),
+	    sizeof(struct ifg_req));
+	if (ifgr.ifgr_groups == NULL)
+		err(1, "getifgroups");
+	if (ioctl(s, SIOCGIFGROUP, (caddr_t)&ifgr) == -1)
+		err(1, "SIOCGIFGROUP");
+
+	ifg = ifgr.ifgr_groups;
+	for (; ifg && len >= sizeof(struct ifg_req); ifg++) {
+		len -= sizeof(struct ifg_req);
+		if (strcmp(ifg->ifgrq_group, groupname) == 0) {
+			ret = 1;
+			break;
+		}
+	}
+	free(ifgr.ifgr_groups);
+
+end:
+	close(s);
+	return (ret);
+}
+
+int
+get_addrs(const char *addr, struct addresslist *al, in_port_t port)
+{
+	if (strcmp("", addr) == 0) {
+		if (host("0.0.0.0", al, 1, port, "0.0.0.0", -1) <= 0) {
+			yyerror("invalid listen ip: %s",
+			    "0.0.0.0");
+			return (-1);
+		}
+		if (host("::", al, 1, port, "::", -1) <= 0) {
+			yyerror("invalid listen ip: %s", "::");
+			return (-1);
+		}
+	} else {
+		if (host(addr, al, GOTWEBD_MAXIFACE, port, addr,
+		    -1) <= 0) {
+			yyerror("invalid listen ip: %s", addr);
+			return (-1);
+		}
+	}
+	return (0);
+}
blob - /dev/null
blob + 037993ed0667b68af3bc09186201595197a814c9 (mode 644)
--- /dev/null
+++ gotwebd/proc.c
@@ -0,0 +1,837 @@
+/*
+ * Copyright (c) 2010 - 2016 Reyk Floeter <reyk@openbsd.org>
+ * Copyright (c) 2008 Pierre-Yves Ritschard <pyr@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <signal.h>
+#include <pwd.h>
+#include <event.h>
+#include <imsg.h>
+
+#include "proc.h"
+
+void	 proc_exec(struct privsep *, struct privsep_proc *, unsigned int,
+	    int, char **);
+void	 proc_setup(struct privsep *, struct privsep_proc *, unsigned int);
+void	 proc_open(struct privsep *, int, int);
+void	 proc_accept(struct privsep *, int, enum privsep_procid,
+	    unsigned int);
+void	 proc_close(struct privsep *);
+int	 proc_ispeer(struct privsep_proc *, unsigned int, enum privsep_procid);
+void	 proc_shutdown(struct privsep_proc *);
+void	 proc_sig_handler(int, short, void *);
+int	 proc_dispatch_null(int, struct privsep_proc *, struct imsg *);
+
+enum privsep_procid privsep_process;
+
+int
+proc_ispeer(struct privsep_proc *procs, unsigned int nproc,
+    enum privsep_procid type)
+{
+	unsigned int i;
+
+	for (i = 0; i < nproc; i++)
+		if (procs[i].p_id == type)
+			return (1);
+
+	return (0);
+}
+
+enum privsep_procid
+proc_getid(struct privsep_proc *procs, unsigned int nproc,
+    const char *proc_name)
+{
+	struct privsep_proc *p;
+	unsigned int proc;
+
+	for (proc = 0; proc < nproc; proc++) {
+		p = &procs[proc];
+		if (strcmp(p->p_title, proc_name))
+			continue;
+
+		return (p->p_id);
+	}
+
+	return (PROC_MAX);
+}
+
+void
+proc_exec(struct privsep *ps, struct privsep_proc *procs, unsigned int nproc,
+    int argc, char **argv)
+{
+	unsigned int proc, nargc, i, proc_i;
+	char **nargv;
+	struct privsep_proc *p;
+	char num[32];
+	int fd;
+
+	/* Prepare the new process argv. */
+	nargv = calloc(argc + 5, sizeof(char *));
+	if (nargv == NULL)
+		fatal("%s: calloc", __func__);
+
+	/* Copy call argument first. */
+	nargc = 0;
+	nargv[nargc++] = argv[0];
+
+	/* Set process name argument and save the position. */
+	nargv[nargc] = strdup("-P");
+	if (nargv[nargc] == NULL)
+		fatal("%s: strdup", __func__);
+	nargc++;
+	proc_i = nargc;
+	nargc++;
+
+	/* Point process instance arg to stack and copy the original args. */
+	nargv[nargc] = strdup("-I");
+	if (nargv[nargc] == NULL)
+		fatal("%s: strdup", __func__);
+	nargc++;
+	nargv[nargc++] = num;
+	for (i = 1; i < (unsigned int) argc; i++)
+		nargv[nargc++] = argv[i];
+
+	nargv[nargc] = NULL;
+
+	for (proc = 0; proc < nproc; proc++) {
+		p = &procs[proc];
+
+		/* Update args with process title. */
+		nargv[proc_i] = (char *)(uintptr_t)p->p_title;
+
+		/* Fire children processes. */
+		for (i = 0; i < ps->ps_instances[p->p_id]; i++) {
+			/* Update the process instance number. */
+			snprintf(num, sizeof(num), "%u", i);
+
+			fd = ps->ps_pipes[p->p_id][i].pp_pipes[PROC_GOTWEBD][0];
+			ps->ps_pipes[p->p_id][i].pp_pipes[PROC_GOTWEBD][0] = -1;
+
+			switch (fork()) {
+			case -1:
+				fatal("%s: fork", __func__);
+				break;
+			case 0:
+				/* First create a new session */
+				if (setsid() == -1)
+					fatal("setsid");
+
+				/* Prepare parent socket. */
+				if (fd != PROC_GOTWEBD_SOCK_FILENO) {
+					if (dup2(fd, PROC_GOTWEBD_SOCK_FILENO)
+					    == -1)
+						fatal("dup2");
+				} else if (fcntl(fd, F_SETFD, 0) == -1)
+					fatal("fcntl");
+
+				execvp(argv[0], nargv);
+				fatal("%s: execvp", __func__);
+				break;
+			default:
+				/* Close child end. */
+				close(fd);
+				break;
+			}
+		}
+	}
+
+	free(nargv);
+}
+
+void
+proc_connect(struct privsep *ps)
+{
+	struct imsgev *iev;
+	unsigned int src, dst, inst;
+
+	/* Don't distribute any sockets if we are not really going to run. */
+	if (ps->ps_noaction)
+		return;
+
+	for (dst = 0; dst < PROC_MAX; dst++) {
+		/* We don't communicate with ourselves. */
+		if (dst == PROC_GOTWEBD)
+			continue;
+
+		for (inst = 0; inst < ps->ps_instances[dst]; inst++) {
+			iev = &ps->ps_ievs[dst][inst];
+			imsg_init(&iev->ibuf, ps->ps_pp->pp_pipes[dst][inst]);
+			event_set(&iev->ev, iev->ibuf.fd, iev->events,
+			    iev->handler, iev->data);
+			event_add(&iev->ev, NULL);
+		}
+	}
+
+	/* Distribute the socketpair()s for everyone. */
+	for (src = 0; src < PROC_MAX; src++)
+		for (dst = src; dst < PROC_MAX; dst++) {
+			/* Parent already distributed its fds. */
+			if (src == PROC_GOTWEBD || dst == PROC_GOTWEBD)
+				continue;
+
+			proc_open(ps, src, dst);
+		}
+}
+
+void
+proc_init(struct privsep *ps, struct privsep_proc *procs, unsigned int nproc,
+    int argc, char **argv, enum privsep_procid proc_id)
+{
+	struct privsep_proc *p = NULL;
+	struct privsep_pipes *pa, *pb;
+	unsigned int proc;
+	unsigned int dst;
+	int fds[2];
+
+	/* Don't initiate anything if we are not really going to run. */
+	if (ps->ps_noaction)
+		return;
+
+	if (proc_id == PROC_GOTWEBD) {
+		privsep_process = PROC_GOTWEBD;
+		proc_setup(ps, procs, nproc);
+
+		/*
+		 * Create the children sockets so we can use them 
+		 * to distribute the rest of the socketpair()s using
+		 * proc_connect() later.
+		 */
+		for (dst = 0; dst < PROC_MAX; dst++) {
+			/* Don't create socket for ourselves. */
+			if (dst == PROC_GOTWEBD)
+				continue;
+
+			for (proc = 0; proc < ps->ps_instances[dst]; proc++) {
+				pa = &ps->ps_pipes[PROC_GOTWEBD][0];
+				pb = &ps->ps_pipes[dst][proc];
+				if (socketpair(AF_UNIX,
+				    SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
+				    PF_UNSPEC, fds) == -1)
+					fatal("%s: socketpair", __func__);
+
+				pa->pp_pipes[dst][proc] = fds[0];
+				pb->pp_pipes[PROC_GOTWEBD][0] = fds[1];
+			}
+		}
+
+		/* Engage! */
+		proc_exec(ps, procs, nproc, argc, argv);
+		return;
+	}
+
+	/* Initialize a child */
+	for (proc = 0; proc < nproc; proc++) {
+		if (procs[proc].p_id != proc_id)
+			continue;
+		p = &procs[proc];
+		break;
+	}
+	if (p == NULL || p->p_init == NULL)
+		fatalx("%s: process %d missing process initialization",
+		    __func__, proc_id);
+
+	p->p_init(ps, p);
+
+	fatalx("failed to initiate child process");
+}
+
+void
+proc_accept(struct privsep *ps, int fd, enum privsep_procid dst,
+    unsigned int n)
+{
+	struct privsep_pipes *pp = ps->ps_pp;
+	struct imsgev *iev;
+
+	if (ps->ps_ievs[dst] == NULL) {
+#if DEBUG > 1
+		log_debug("%s: %s src %d %d to dst %d %d not connected",
+		    __func__, ps->ps_title[privsep_process],
+		    privsep_process, ps->ps_instance + 1,
+		    dst, n + 1);
+#endif
+		close(fd);
+		return;
+	}
+
+	if (pp->pp_pipes[dst][n] != -1) {
+		log_warnx("%s: duplicated descriptor", __func__);
+		close(fd);
+		return;
+	} else
+		pp->pp_pipes[dst][n] = fd;
+
+	iev = &ps->ps_ievs[dst][n];
+	imsg_init(&iev->ibuf, fd);
+	event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev->data);
+	event_add(&iev->ev, NULL);
+}
+
+void
+proc_setup(struct privsep *ps, struct privsep_proc *procs, unsigned int nproc)
+{
+	unsigned int i, j, src, dst, id;
+	struct privsep_pipes *pp;
+
+	/* Initialize parent title, ps_instances and procs. */
+	ps->ps_title[PROC_GOTWEBD] = "parent";
+
+	for (src = 0; src < PROC_MAX; src++)
+		/* Default to 1 process instance */
+		if (ps->ps_instances[src] < 1)
+			ps->ps_instances[src] = 1;
+
+	for (src = 0; src < nproc; src++) {
+		procs[src].p_ps = ps;
+		if (procs[src].p_cb == NULL)
+			procs[src].p_cb = proc_dispatch_null;
+
+		id = procs[src].p_id;
+		ps->ps_title[id] = procs[src].p_title;
+		ps->ps_ievs[id] = calloc(ps->ps_instances[id],
+		    sizeof(struct imsgev));
+		if (ps->ps_ievs[id] == NULL)
+			fatal("%s: calloc", __func__);
+
+		/* With this set up, we are ready to call imsg_init(). */
+		for (i = 0; i < ps->ps_instances[id]; i++) {
+			ps->ps_ievs[id][i].handler = proc_dispatch;
+			ps->ps_ievs[id][i].events = EV_READ;
+			ps->ps_ievs[id][i].proc = &procs[src];
+			ps->ps_ievs[id][i].data = &ps->ps_ievs[id][i];
+		}
+	}
+
+	/*
+	 * Allocate pipes for all process instances (incl. parent)
+	 *
+	 * - ps->ps_pipes: N:M mapping
+	 * N source processes connected to M destination processes:
+	 * [src][instances][dst][instances], for example
+	 * [PROC_RELAY][3][PROC_CA][3]
+	 *
+	 * - ps->ps_pp: per-process 1:M part of ps->ps_pipes
+	 * Each process instance has a destination array of socketpair fds:
+	 * [dst][instances], for example
+	 * [PROC_GOTWEBD][0]
+	 */
+	for (src = 0; src < PROC_MAX; src++) {
+		/* Allocate destination array for each process */
+		ps->ps_pipes[src] = calloc(ps->ps_instances[src],
+		    sizeof(struct privsep_pipes));
+		if (ps->ps_pipes[src] == NULL)
+			fatal("%s: calloc", __func__);
+
+		for (i = 0; i < ps->ps_instances[src]; i++) {
+			pp = &ps->ps_pipes[src][i];
+
+			for (dst = 0; dst < PROC_MAX; dst++) {
+				/* Allocate maximum fd integers */
+				pp->pp_pipes[dst] =
+				    calloc(ps->ps_instances[dst],
+				    sizeof(int));
+				if (pp->pp_pipes[dst] == NULL)
+					fatal("%s: calloc", __func__);
+
+				/* Mark fd as unused */
+				for (j = 0; j < ps->ps_instances[dst]; j++)
+					pp->pp_pipes[dst][j] = -1;
+			}
+		}
+	}
+
+	ps->ps_pp = &ps->ps_pipes[privsep_process][ps->ps_instance];
+}
+
+void
+proc_kill(struct privsep *ps)
+{
+	char *cause;
+	pid_t pid;
+	int len, status;
+
+	if (privsep_process != PROC_GOTWEBD)
+		return;
+
+	proc_close(ps);
+
+	do {
+		pid = waitpid(WAIT_ANY, &status, 0);
+		if (pid <= 0)
+			continue;
+
+		if (WIFSIGNALED(status)) {
+			len = asprintf(&cause, "terminated; signal %d",
+			    WTERMSIG(status));
+		} else if (WIFEXITED(status)) {
+			if (WEXITSTATUS(status) != 0)
+				len = asprintf(&cause, "exited abnormally");
+			else
+				len = 0;
+		} else
+			len = -1;
+
+		if (len == 0) {
+			/* child exited OK, don't print a warning message */
+		} else if (len != -1) {
+			log_warnx("lost child: pid %u %s", pid, cause);
+			free(cause);
+		} else
+			log_warnx("lost child: pid %u", pid);
+	} while (pid != -1 || (pid == -1 && errno == EINTR));
+}
+
+void
+proc_open(struct privsep *ps, int src, int dst)
+{
+	struct privsep_pipes *pa, *pb;
+	struct privsep_fd pf;
+	int fds[2];
+	unsigned int i, j;
+
+	/* Exchange pipes between process. */
+	for (i = 0; i < ps->ps_instances[src]; i++) {
+		for (j = 0; j < ps->ps_instances[dst]; j++) {
+			/* Don't create sockets for ourself. */
+			if (src == dst && i == j)
+				continue;
+
+			pa = &ps->ps_pipes[src][i];
+			pb = &ps->ps_pipes[dst][j];
+			if (socketpair(AF_UNIX,
+			    SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC,
+			    PF_UNSPEC, fds) == -1)
+				fatal("%s: socketpair", __func__);
+
+			pa->pp_pipes[dst][j] = fds[0];
+			pb->pp_pipes[src][i] = fds[1];
+
+			pf.pf_procid = src;
+			pf.pf_instance = i;
+			if (proc_compose_imsg(ps, dst, j, IMSG_CTL_PROCFD,
+			    -1, pb->pp_pipes[src][i], &pf, sizeof(pf)) == -1)
+				fatal("%s: proc_compose_imsg", __func__);
+
+			pf.pf_procid = dst;
+			pf.pf_instance = j;
+			if (proc_compose_imsg(ps, src, i, IMSG_CTL_PROCFD,
+			    -1, pa->pp_pipes[dst][j], &pf, sizeof(pf)) == -1)
+				fatal("%s: proc_compose_imsg", __func__);
+
+			/*
+			 * We have to flush to send the descriptors and close
+			 * them to avoid the fd ramp on startup.
+			 */
+			if (proc_flush_imsg(ps, src, i) == -1 ||
+			    proc_flush_imsg(ps, dst, j) == -1)
+				fatal("%s: imsg_flush", __func__);
+		}
+	}
+}
+
+void
+proc_close(struct privsep *ps)
+{
+	unsigned int dst, n;
+	struct privsep_pipes *pp;
+
+	if (ps == NULL)
+		return;
+
+	pp = ps->ps_pp;
+
+	for (dst = 0; dst < PROC_MAX; dst++) {
+		if (ps->ps_ievs[dst] == NULL)
+			continue;
+
+		for (n = 0; n < ps->ps_instances[dst]; n++) {
+			if (pp->pp_pipes[dst][n] == -1)
+				continue;
+
+			/* Cancel the fd, close and invalidate the fd */
+			event_del(&(ps->ps_ievs[dst][n].ev));
+			imsg_clear(&(ps->ps_ievs[dst][n].ibuf));
+			close(pp->pp_pipes[dst][n]);
+			pp->pp_pipes[dst][n] = -1;
+		}
+		free(ps->ps_ievs[dst]);
+	}
+}
+
+void
+proc_shutdown(struct privsep_proc *p)
+{
+	struct privsep *ps = p->p_ps;
+
+	if (p->p_shutdown != NULL)
+		(*p->p_shutdown)();
+
+	proc_close(ps);
+
+	log_info("%s, %s exiting, pid %d", getprogname(), p->p_title, getpid());
+
+	exit(0);
+}
+
+void
+proc_sig_handler(int sig, short event, void *arg)
+{
+	struct privsep_proc *p = arg;
+
+	switch (sig) {
+	case SIGINT:
+	case SIGTERM:
+		proc_shutdown(p);
+		break;
+	case SIGCHLD:
+	case SIGHUP:
+	case SIGPIPE:
+	case SIGUSR1:
+		/* ignore */
+		break;
+	default:
+		fatalx("proc_sig_handler: unexpected signal");
+		/* NOTREACHED */
+	}
+}
+
+void
+proc_run(struct privsep *ps, struct privsep_proc *p,
+    struct privsep_proc *procs, unsigned int nproc,
+    void (*run)(struct privsep *, struct privsep_proc *, void *), void *arg)
+{
+	struct passwd *pw;
+	const char *root;
+
+	log_procinit(p->p_title);
+
+	/* Set the process group of the current process */
+	setpgid(0, 0);
+
+	/* Use non-standard user */
+	if (p->p_pw != NULL)
+		pw = p->p_pw;
+	else
+		pw = ps->ps_pw;
+
+	/* Change root directory */
+	if (p->p_chroot != NULL)
+		root = p->p_chroot;
+	else
+		root = pw->pw_dir;
+
+	if (chroot(root) == -1)
+		fatal("proc_run: chroot");
+	if (chdir("/") == -1)
+		fatal("proc_run: chdir(\"/\")");
+
+	privsep_process = p->p_id;
+
+	setproctitle("%s", p->p_title);
+
+	if (setgroups(1, &pw->pw_gid) ||
+	    setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) ||
+	    setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid))
+		fatal("proc_run: cannot drop privileges");
+
+	event_init();
+
+	signal_set(&ps->ps_evsigint, SIGINT, proc_sig_handler, p);
+	signal_set(&ps->ps_evsigterm, SIGTERM, proc_sig_handler, p);
+	signal_set(&ps->ps_evsigchld, SIGCHLD, proc_sig_handler, p);
+	signal_set(&ps->ps_evsighup, SIGHUP, proc_sig_handler, p);
+	signal_set(&ps->ps_evsigpipe, SIGPIPE, proc_sig_handler, p);
+	signal_set(&ps->ps_evsigusr1, SIGUSR1, proc_sig_handler, p);
+
+	signal_add(&ps->ps_evsigint, NULL);
+	signal_add(&ps->ps_evsigterm, NULL);
+	signal_add(&ps->ps_evsigchld, NULL);
+	signal_add(&ps->ps_evsighup, NULL);
+	signal_add(&ps->ps_evsigpipe, NULL);
+	signal_add(&ps->ps_evsigusr1, NULL);
+
+	proc_setup(ps, procs, nproc);
+	proc_accept(ps, PROC_GOTWEBD_SOCK_FILENO, PROC_GOTWEBD, 0);
+
+	DPRINTF("%s: %s %d/%d, pid %d", __func__, p->p_title,
+	    ps->ps_instance + 1, ps->ps_instances[p->p_id], getpid());
+
+	if (run != NULL)
+		run(ps, p, arg);
+
+	event_dispatch();
+
+	proc_shutdown(p);
+}
+
+void
+proc_dispatch(int fd, short event, void *arg)
+{
+	struct imsgev *iev = arg;
+	struct privsep_proc *p = iev->proc;
+	struct privsep *ps = p->p_ps;
+	struct imsgbuf *ibuf;
+	struct imsg imsg;
+	ssize_t n;
+	int verbose;
+	const char *title;
+	struct privsep_fd pf;
+
+	title = ps->ps_title[privsep_process];
+	ibuf = &iev->ibuf;
+
+	if (event & EV_READ) {
+		n = imsg_read(ibuf);
+		if (n == -1 && errno != EAGAIN)
+			fatal("%s: imsg_read", __func__);
+		if (n == 0) {
+			/* this pipe is dead, so remove the event handler */
+			event_del(&iev->ev);
+			event_loopexit(NULL);
+			return;
+		}
+	}
+
+	if (event & EV_WRITE) {
+		n = msgbuf_write(&ibuf->w);
+		if (n == -1 && errno != EAGAIN)
+			fatal("%s: msgbuf_write", __func__);
+		if (n == 0) {
+			/* this pipe is dead, so remove the event handler */
+			event_del(&iev->ev);
+			event_loopexit(NULL);
+			return;
+		}
+	}
+
+	for (;;) {
+		n = imsg_get(ibuf, &imsg);
+		if (n == -1)
+			fatal("%s: imsg_get", __func__);
+		if (n == 0)
+			break;
+
+#if DEBUG > 1
+		log_debug("%s: %s %d got imsg %d peerid %d from %s %d",
+		    __func__, title, ps->ps_instance + 1,
+		    imsg.hdr.type, imsg.hdr.peerid, p->p_title, imsg.hdr.pid);
+#endif
+
+		/*
+		 * Check the message with the program callback
+		 */
+		if ((p->p_cb)(fd, p, &imsg) == 0) {
+			/* Message was handled by the callback, continue */
+			imsg_free(&imsg);
+			continue;
+		}
+
+		/*
+		 * Generic message handling
+		 */
+		switch (imsg.hdr.type) {
+		case IMSG_CTL_VERBOSE:
+			log_info("%s", __func__);
+			IMSG_SIZE_CHECK(&imsg, &verbose);
+			memcpy(&verbose, imsg.data, sizeof(verbose));
+			log_setverbose(verbose);
+			break;
+		case IMSG_CTL_PROCFD:
+			IMSG_SIZE_CHECK(&imsg, &pf);
+			memcpy(&pf, imsg.data, sizeof(pf));
+			proc_accept(ps, imsg.fd, pf.pf_procid,
+			    pf.pf_instance);
+			break;
+		default:
+			log_warnx("%s: %s %d got invalid imsg %d peerid %d "
+			    "from %s %d",
+			    __func__, title, ps->ps_instance + 1,
+			    imsg.hdr.type, imsg.hdr.peerid,
+			    p->p_title, imsg.hdr.pid);
+		}
+		imsg_free(&imsg);
+	}
+	imsg_event_add(iev);
+}
+
+int
+proc_dispatch_null(int fd, struct privsep_proc *p, struct imsg *imsg)
+{
+	return (-1);
+}
+
+/*
+ * imsg helper functions
+ */
+
+void
+imsg_event_add(struct imsgev *iev)
+{
+	if (iev->handler == NULL) {
+		imsg_flush(&iev->ibuf);
+		return;
+	}
+
+	iev->events = EV_READ;
+	if (iev->ibuf.w.queued)
+		iev->events |= EV_WRITE;
+
+	event_del(&iev->ev);
+	event_set(&iev->ev, iev->ibuf.fd, iev->events, iev->handler, iev->data);
+	event_add(&iev->ev, NULL);
+}
+
+int
+imsg_compose_event(struct imsgev *iev, uint16_t type, uint32_t peerid,
+    pid_t pid, int fd, void *data, uint16_t datalen)
+{
+	int ret;
+
+	ret = imsg_compose(&iev->ibuf, type, peerid, pid, fd, data, datalen);
+	if (ret == -1)
+		return (ret);
+	imsg_event_add(iev);
+	return (ret);
+}
+
+int
+imsg_composev_event(struct imsgev *iev, uint16_t type, uint32_t peerid,
+    pid_t pid, int fd, const struct iovec *iov, int iovcnt)
+{
+	int ret;
+
+	ret = imsg_composev(&iev->ibuf, type, peerid, pid, fd, iov, iovcnt);
+	if (ret == -1)
+		return (ret);
+	imsg_event_add(iev);
+	return (ret);
+}
+
+void
+proc_range(struct privsep *ps, enum privsep_procid id, int *n, int *m)
+{
+	if (*n == -1) {
+		/* Use a range of all target instances */
+		*n = 0;
+		*m = ps->ps_instances[id];
+	} else {
+		/* Use only a single slot of the specified peer process */
+		*m = *n + 1;
+	}
+}
+
+int
+proc_compose_imsg(struct privsep *ps, enum privsep_procid id, int n,
+    uint16_t type, uint32_t peerid, int fd, void *data, uint16_t datalen)
+{
+	int m;
+
+	proc_range(ps, id, &n, &m);
+	for (; n < m; n++) {
+		if (imsg_compose_event(&ps->ps_ievs[id][n],
+		    type, peerid, ps->ps_instance + 1, fd, data, datalen) == -1)
+			return (-1);
+	}
+
+	return (0);
+}
+
+int
+proc_compose(struct privsep *ps, enum privsep_procid id,
+    uint16_t type, void *data, uint16_t datalen)
+{
+	return (proc_compose_imsg(ps, id, -1, type, -1, -1, data, datalen));
+}
+
+int
+proc_composev_imsg(struct privsep *ps, enum privsep_procid id, int n,
+    uint16_t type, uint32_t peerid, int fd, const struct iovec *iov, int iovcnt)
+{
+	int m;
+
+	proc_range(ps, id, &n, &m);
+	for (; n < m; n++)
+		if (imsg_composev_event(&ps->ps_ievs[id][n],
+		    type, peerid, ps->ps_instance + 1, fd, iov, iovcnt) == -1)
+			return (-1);
+
+	return (0);
+}
+
+int
+proc_composev(struct privsep *ps, enum privsep_procid id,
+    uint16_t type, const struct iovec *iov, int iovcnt)
+{
+	return (proc_composev_imsg(ps, id, -1, type, -1, -1, iov, iovcnt));
+}
+
+int
+proc_forward_imsg(struct privsep *ps, struct imsg *imsg,
+    enum privsep_procid id, int n)
+{
+	return (proc_compose_imsg(ps, id, n, imsg->hdr.type,
+	    imsg->hdr.peerid, imsg->fd, imsg->data, IMSG_DATA_SIZE(imsg)));
+}
+
+struct imsgbuf *
+proc_ibuf(struct privsep *ps, enum privsep_procid id, int n)
+{
+	int m;
+
+	proc_range(ps, id, &n, &m);
+	return (&ps->ps_ievs[id][n].ibuf);
+}
+
+struct imsgev *
+proc_iev(struct privsep *ps, enum privsep_procid id, int n)
+{
+	int m;
+
+	proc_range(ps, id, &n, &m);
+	return (&ps->ps_ievs[id][n]);
+}
+
+/* This function should only be called with care as it breaks async I/O */
+int
+proc_flush_imsg(struct privsep *ps, enum privsep_procid id, int n)
+{
+	struct imsgbuf *ibuf;
+	int m, ret = 0;
+
+	proc_range(ps, id, &n, &m);
+	for (; n < m; n++) {
+		ibuf = proc_ibuf(ps, id, n);
+		if (ibuf == NULL)
+			return (-1);
+		do {
+			ret = imsg_flush(ibuf);
+		} while (ret == -1 && errno == EAGAIN);
+		if (ret == -1)
+			break;
+		imsg_event_add(&ps->ps_ievs[id][n]);
+	}
+
+	return (ret);
+}
blob - /dev/null
blob + 524112fcf4b0c72a25c6f5a4cb85c16bd33ef3a9 (mode 644)
--- /dev/null
+++ gotwebd/proc.h
@@ -0,0 +1,176 @@
+/*
+ * Copyright (c) 2010-2015 Reyk Floeter <reyk@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+enum {
+	IMSG_NONE,
+	IMSG_CTL_OK,
+	IMSG_CTL_FAIL,
+	IMSG_CTL_VERBOSE,
+	IMSG_CTL_NOTIFY,
+	IMSG_CTL_RESET,
+	IMSG_CTL_PROCFD,
+	IMSG_PROC_MAX
+};
+
+/* imsg */
+struct imsgev {
+	struct imsgbuf		 ibuf;
+	void			(*handler)(int, short, void *);
+	struct event		 ev;
+	struct privsep_proc	*proc;
+	void			*data;
+	short			 events;
+};
+
+#define IMSG_SIZE_CHECK(imsg, p) do {					\
+	if (IMSG_DATA_SIZE(imsg) < sizeof(*p))				\
+		fatalx("bad length imsg received (%s)",	#p);		\
+} while (0)
+#define IMSG_DATA_SIZE(imsg)	((imsg)->hdr.len - IMSG_HEADER_SIZE)
+
+struct ctl_conn {
+	TAILQ_ENTRY(ctl_conn)	 entry;
+	uint8_t			 flags;
+	unsigned int		 waiting;
+#define CTL_CONN_NOTIFY		 0x01
+	struct imsgev		 iev;
+	uid_t			 uid;
+};
+TAILQ_HEAD(ctl_connlist, ctl_conn);
+extern  struct ctl_connlist ctl_conns;
+
+/* privsep */
+enum privsep_procid {
+	PROC_GOTWEBD	= 0,
+	PROC_SOCKS,
+	PROC_MAX,
+};
+extern enum privsep_procid privsep_process;
+
+#define CONFIG_RELOAD		0x00
+#define CONFIG_SOCKS		0x01
+#define CONFIG_ALL		0xff
+
+struct privsep_pipes {
+	int				*pp_pipes[PROC_MAX];
+};
+
+struct privsep {
+	struct privsep_pipes		*ps_pipes[PROC_MAX];
+	struct privsep_pipes		*ps_pp;
+
+	struct imsgev			*ps_ievs[PROC_MAX];
+	const char			*ps_title[PROC_MAX];
+	uint8_t				 ps_what[PROC_MAX];
+
+	struct passwd			*ps_pw;
+	int				 ps_noaction;
+
+	unsigned int			 ps_instances[PROC_MAX];
+	unsigned int			 ps_instance;
+
+	/* Event and signal handlers */
+	struct event			 ps_evsigint;
+	struct event			 ps_evsigterm;
+	struct event			 ps_evsigchld;
+	struct event			 ps_evsighup;
+	struct event			 ps_evsigpipe;
+	struct event			 ps_evsigusr1;
+
+	void				*ps_env;
+};
+
+struct privsep_proc {
+	const char		*p_title;
+	enum privsep_procid	 p_id;
+	int			(*p_cb)(int, struct privsep_proc *,
+				    struct imsg *);
+	void			(*p_init)(struct privsep *,
+				    struct privsep_proc *);
+	void			(*p_shutdown)(void);
+	const char		*p_chroot;
+	struct passwd		*p_pw;
+	struct privsep		*p_ps;
+};
+
+struct privsep_fd {
+	enum privsep_procid		 pf_procid;
+	unsigned int			 pf_instance;
+};
+
+#if DEBUG
+#define DPRINTF		log_debug
+#else
+#define DPRINTF(x...)	do {} while(0)
+#endif
+
+#define PROC_GOTWEBD_SOCK_FILENO 3
+#define PROC_MAX_INSTANCES      32
+
+/* proc.c */
+void	 proc_init(struct privsep *, struct privsep_proc *, unsigned int,
+	    int, char **, enum privsep_procid);
+void	 proc_kill(struct privsep *);
+void	 proc_connect(struct privsep *ps);
+void	 proc_dispatch(int, short event, void *);
+void	 proc_range(struct privsep *, enum privsep_procid, int *, int *);
+void	 proc_run(struct privsep *, struct privsep_proc *,
+	    struct privsep_proc *, unsigned int,
+	    void (*)(struct privsep *, struct privsep_proc *, void *), void *);
+void	 imsg_event_add(struct imsgev *);
+int	 imsg_compose_event(struct imsgev *, uint16_t, uint32_t,
+	    pid_t, int, void *, uint16_t);
+int	 imsg_composev_event(struct imsgev *, uint16_t, uint32_t,
+	    pid_t, int, const struct iovec *, int);
+int	 proc_compose_imsg(struct privsep *, enum privsep_procid, int,
+	    uint16_t, uint32_t, int, void *, uint16_t);
+int	 proc_compose(struct privsep *, enum privsep_procid,
+	    uint16_t, void *data, uint16_t);
+int	 proc_composev_imsg(struct privsep *, enum privsep_procid, int,
+	    uint16_t, uint32_t, int, const struct iovec *, int);
+int	 proc_composev(struct privsep *, enum privsep_procid,
+	    uint16_t, const struct iovec *, int);
+int	 proc_forward_imsg(struct privsep *, struct imsg *,
+	    enum privsep_procid, int);
+struct imsgbuf *
+	 proc_ibuf(struct privsep *, enum privsep_procid, int);
+struct imsgev *
+	 proc_iev(struct privsep *, enum privsep_procid, int);
+enum privsep_procid
+	 proc_getid(struct privsep_proc *, unsigned int, const char *);
+int	 proc_flush_imsg(struct privsep *, enum privsep_procid, int);
+
+/* log.c */
+void	log_init(int, int);
+void	log_procinit(const char *);
+void	log_setverbose(int);
+int	log_getverbose(void);
+void	log_warn(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_warnx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_info(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	log_debug(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void	logit(int, const char *, ...)
+	    __attribute__((__format__ (printf, 2, 3)));
+void	vlog(int, const char *, va_list)
+	    __attribute__((__format__ (printf, 2, 0)));
+__dead void fatal(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+__dead void fatalx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
blob - /dev/null
blob + f07a3be3b97818f5ddfc016d4f5b93952636de34 (mode 644)
--- /dev/null
+++ gotwebd/sockets.c
@@ -0,0 +1,724 @@
+/*
+ * Copyright (c) 2016, 2019, 2020-2021 Tracey Emery <tracey@traceyemery.net>
+ * Copyright (c) 2015 Mike Larkin <mlarkin@openbsd.org>
+ * Copyright (c) 2013 David Gwynne <dlg@openbsd.org>
+ * Copyright (c) 2013 Florian Obser <florian@openbsd.org>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/param.h>
+#include <sys/ioctl.h>
+#include <sys/queue.h>
+#include <sys/wait.h>
+#include <sys/uio.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <sys/un.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include <errno.h>
+#include <event.h>
+#include <fcntl.h>
+#include <ifaddrs.h>
+#include <imsg.h>
+#include <limits.h>
+#include <netdb.h>
+#include <poll.h>
+#include <pwd.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <util.h>
+
+#include "got_error.h"
+#include "got_opentemp.h"
+
+#include "proc.h"
+#include "gotwebd.h"
+
+#define SOCKS_BACKLOG 5
+#define MAXIMUM(a, b)	(((a) > (b)) ? (a) : (b))
+
+
+volatile int client_cnt;
+
+struct timeval	timeout = { TIMEOUT_DEFAULT, 0 };
+
+void	 sockets_sighdlr(int, short, void *);
+void	 sockets_run(struct privsep *, struct privsep_proc *, void *);
+void	 sockets_launch(void);
+void	 sockets_purge(struct gotwebd *);
+void	 sockets_accept_paused(int, short, void *);
+void	 sockets_dup_new_socket(struct socket *, struct socket *);
+void	 sockets_rlimit(int);
+
+
+int	 sockets_dispatch_gotwebd(int, struct privsep_proc *, struct imsg *);
+int	 sockets_unix_socket_listen(struct privsep *, struct socket *);
+int	 sockets_create_socket(struct addresslist *, in_port_t);
+int	 sockets_socket_af(struct sockaddr_storage *, in_port_t);
+int	 sockets_accept_reserve(int, struct sockaddr *, socklen_t *, int,
+	    volatile int *);
+
+struct socket
+	*sockets_conf_new_socket(struct gotwebd *, struct server *, int, int,
+	    int);
+
+int cgi_inflight = 0;
+
+static struct privsep_proc procs[] = {
+	{ "gotwebd",	PROC_GOTWEBD,	sockets_dispatch_gotwebd  },
+};
+
+void
+sockets(struct privsep *ps, struct privsep_proc *p)
+{
+	proc_run(ps, p, procs, nitems(procs), sockets_run, NULL);
+}
+
+void
+sockets_run(struct privsep *ps, struct privsep_proc *p, void *arg)
+{
+	if (config_init(ps->ps_env) == -1)
+		fatal("failed to initialize configuration");
+
+	p->p_shutdown = sockets_shutdown;
+
+	sockets_rlimit(-1);
+
+	signal_del(&ps->ps_evsigchld);
+	signal_set(&ps->ps_evsigchld, SIGCHLD, sockets_sighdlr, ps);
+	signal_add(&ps->ps_evsigchld, NULL);
+
+#ifndef PROFILE
+	if (pledge("stdio rpath wpath cpath inet recvfd proc exec sendfd",
+	    NULL) == -1)
+		fatal("pledge");
+#endif
+}
+
+void
+sockets_parse_sockets(struct gotwebd *env)
+{
+	struct server *srv;
+	struct socket *sock, *new_sock = NULL;
+	struct address *a;
+	int sock_id = 0, ipv4 = 0, ipv6 = 0;
+
+	TAILQ_FOREACH(srv, env->servers, entry) {
+		if (srv->unix_socket) {
+			sock_id++;
+			new_sock = sockets_conf_new_socket(env, srv,
+			    sock_id, UNIX, 0);
+			TAILQ_INSERT_TAIL(env->sockets, new_sock, entry);
+		}
+
+		if (srv->fcgi_socket) {
+			sock_id++;
+			new_sock = sockets_conf_new_socket(env, srv,
+			    sock_id, FCGI, 0);
+			TAILQ_INSERT_TAIL(env->sockets, new_sock, entry);
+
+			/* add ipv6 children */
+			TAILQ_FOREACH(sock, env->sockets, entry) {
+				ipv4 = ipv6 = 0;
+
+				TAILQ_FOREACH(a, sock->conf.al, entry) {
+					if (a->ss.ss_family == AF_INET)
+						ipv4 = 1;
+					if (a->ss.ss_family == AF_INET6)
+						ipv6 = 1;
+				}
+
+				/* create ipv6 sock */
+				if (ipv4 == 1 && ipv6 == 1) {
+					sock_id++;
+					sock->conf.child_id = sock_id;
+					new_sock = sockets_conf_new_socket(env,
+					    srv, sock_id, FCGI, 1);
+					sockets_dup_new_socket(sock, new_sock);
+					TAILQ_INSERT_TAIL(env->sockets,
+					    new_sock, entry);
+					continue;
+				}
+			}
+		}
+	}
+}
+
+void
+sockets_dup_new_socket(struct socket *p_sock, struct socket *sock)
+{
+	struct address *a, *acp;
+	int n;
+
+	sock->conf.parent_id = p_sock->conf.id;
+	sock->conf.ipv4 = 0;
+	sock->conf.ipv6 = 1;
+
+	memcpy(&sock->conf.srv_name, p_sock->conf.srv_name,
+	    sizeof(sock->conf.srv_name));
+
+	n = snprintf(sock->conf.name, GOTWEBD_MAXTEXT, "%s_child",
+	    p_sock->conf.srv_name);
+	if (n < 0) {
+		free(p_sock->conf.al);
+		free(p_sock);
+		free(sock->conf.al);
+		free(sock);
+		fatalx("%s: snprintf", __func__);
+	}
+
+	TAILQ_FOREACH(a, p_sock->conf.al, entry) {
+		if (a->ss.ss_family == AF_INET)
+			continue;
+
+		if ((acp = calloc(1, sizeof(*acp))) == NULL)
+			fatal("%s: calloc", __func__);
+		memcpy(&acp->ss, &a->ss, sizeof(acp->ss));
+		acp->ipproto = a->ipproto;
+		acp->prefixlen = a->prefixlen;
+		acp->port = a->port;
+		if (strlen(a->ifname) != 0) {
+			if (strlcpy(acp->ifname, a->ifname,
+			    sizeof(acp->ifname)) >= sizeof(acp->ifname)) {
+				fatalx("%s: interface name truncated",
+				    __func__);
+			}
+		}
+
+		TAILQ_INSERT_TAIL(sock->conf.al, acp, entry);
+	}
+}
+
+struct socket *
+sockets_conf_new_socket(struct gotwebd *env, struct server *srv, int id,
+    int type, int is_dup)
+{
+	struct socket *sock;
+	struct address *a, *acp;
+	int n;
+
+	if ((sock = calloc(1, sizeof(*sock))) == NULL)
+		fatalx("%s: calloc", __func__);
+
+	if ((sock->conf.al = calloc(1, sizeof(*sock->conf.al))) == NULL) {
+		free(sock);
+		fatalx("%s: calloc", __func__);
+	}
+	TAILQ_INIT(sock->conf.al);
+
+	sock->conf.parent_id = 0;
+	sock->conf.id = id;
+
+	sock->fd = -1;
+
+	sock->conf.type = type;
+
+	if (type == UNIX) {
+		if (strlcpy(sock->conf.unix_socket_name,
+		    srv->unix_socket_name,
+		    sizeof(sock->conf.unix_socket_name)) >=
+		    sizeof(sock->conf.unix_socket_name)) {
+			free(sock->conf.al);
+			free(sock);
+			fatalx("%s: strlcpy", __func__);
+		}
+	} else
+		sock->conf.ipv4 = 1;
+
+	sock->conf.fcgi_socket_port = srv->fcgi_socket_port;
+
+	if (is_dup)
+		goto done;
+
+	n = snprintf(sock->conf.name, GOTWEBD_MAXTEXT, "%s_parent",
+	    srv->name);
+	if (n < 0) {
+		free(sock->conf.al);
+		free(sock);
+		fatalx("%s: snprintf", __func__);
+	}
+
+	if (strlcpy(sock->conf.srv_name, srv->name,
+	    sizeof(sock->conf.srv_name)) >= sizeof(sock->conf.srv_name)) {
+		free(sock->conf.al);
+		free(sock);
+		fatalx("%s: strlcpy", __func__);
+	}
+
+	TAILQ_FOREACH(a, srv->al, entry) {
+		if ((acp = calloc(1, sizeof(*acp))) == NULL) {
+			free(sock->conf.al);
+			free(sock);
+			fatal("%s: calloc", __func__);
+		}
+		memcpy(&acp->ss, &a->ss, sizeof(acp->ss));
+		acp->ipproto = a->ipproto;
+		acp->prefixlen = a->prefixlen;
+		acp->port = a->port;
+		if (strlen(a->ifname) != 0) {
+			if (strlcpy(acp->ifname, a->ifname,
+			    sizeof(acp->ifname)) >= sizeof(acp->ifname)) {
+				fatalx("%s: interface name truncated",
+				    __func__);
+			}
+		}
+
+		TAILQ_INSERT_TAIL(sock->conf.al, acp, entry);
+	}
+done:
+	return (sock);
+}
+
+int
+sockets_socket_af(struct sockaddr_storage *ss, in_port_t port)
+{
+	switch (ss->ss_family) {
+	case AF_INET:
+		((struct sockaddr_in *)ss)->sin_port = port;
+		((struct sockaddr_in *)ss)->sin_len =
+		    sizeof(struct sockaddr_in);
+		break;
+	case AF_INET6:
+		((struct sockaddr_in6 *)ss)->sin6_port = port;
+		((struct sockaddr_in6 *)ss)->sin6_len =
+		    sizeof(struct sockaddr_in6);
+		break;
+	default:
+		return -1;
+	}
+
+	return 0;
+}
+
+void
+sockets_launch(void)
+{
+	struct socket *sock;
+
+	TAILQ_FOREACH(sock, gotwebd_env->sockets, entry) {
+		log_debug("%s: configuring socket %d (%d)", __func__,
+		    sock->conf.id, sock->fd);
+
+		event_set(&sock->ev, sock->fd, EV_READ | EV_PERSIST,
+		    sockets_socket_accept, sock);
+
+		if (event_add(&sock->ev, NULL))
+			fatalx("event add sock");
+
+		evtimer_set(&sock->pause, sockets_accept_paused, sock);
+
+		log_debug("%s: running socket listener %d", __func__,
+		    sock->conf.id);
+	}
+}
+
+void
+sockets_purge(struct gotwebd *env)
+{
+	struct socket *sock, *tsock;
+
+	/* shutdown and remove sockets */
+	TAILQ_FOREACH_SAFE(sock, env->sockets, entry, tsock) {
+		if (event_initialized(&sock->ev))
+			event_del(&sock->ev);
+		if (evtimer_initialized(&sock->evt))
+			evtimer_del(&sock->evt);
+		if (evtimer_initialized(&sock->pause))
+			evtimer_del(&sock->pause);
+		if (sock->fd != -1)
+			close(sock->fd);
+		TAILQ_REMOVE(env->sockets, sock, entry);
+	}
+}
+
+int
+sockets_dispatch_gotwebd(int fd, struct privsep_proc *p, struct imsg *imsg)
+{
+	struct privsep *ps = p->p_ps;
+	int res = 0, cmd = 0, verbose;
+
+	switch (imsg->hdr.type) {
+	case IMSG_CFG_SRV:
+		config_getserver(gotwebd_env, imsg);
+		break;
+	case IMSG_CFG_SOCK:
+		config_getsock(gotwebd_env, imsg);
+		break;
+	case IMSG_CFG_FD:
+		config_getfd(gotwebd_env, imsg);
+		break;
+	case IMSG_CFG_DONE:
+		config_getcfg(gotwebd_env, imsg);
+		break;
+	case IMSG_CTL_START:
+		sockets_launch();
+		break;
+	case IMSG_CTL_VERBOSE:
+		IMSG_SIZE_CHECK(imsg, &verbose);
+		memcpy(&verbose, imsg->data, sizeof(verbose));
+		log_setverbose(verbose);
+		break;
+	default:
+		return -1;
+	}
+
+	switch (cmd) {
+	case 0:
+		break;
+	default:
+		if (proc_compose_imsg(ps, PROC_GOTWEBD, -1, cmd,
+		    imsg->hdr.peerid, -1, &res, sizeof(res)) == -1)
+			return -1;
+		break;
+	}
+
+	return 0;
+}
+
+void
+sockets_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;
+	default:
+		log_info("SIGNAL: %d", sig);
+		fatalx("unexpected signal");
+	}
+}
+
+void
+sockets_shutdown(void)
+{
+	struct server *srv, *tsrv;
+	struct socket *sock, *tsock;
+
+	sockets_purge(gotwebd_env);
+
+	/* clean sockets */
+	TAILQ_FOREACH_SAFE(sock, gotwebd_env->sockets, entry, tsock) {
+		TAILQ_REMOVE(gotwebd_env->sockets, sock, entry);
+		close(sock->fd);
+		free(sock);
+	}
+
+	/* clean servers */
+	TAILQ_FOREACH_SAFE(srv, gotwebd_env->servers, entry, tsrv)
+		free(srv);
+
+	free(gotwebd_env->sockets);
+	free(gotwebd_env->servers);
+	free(gotwebd_env);
+}
+
+int
+sockets_privinit(struct gotwebd *env, struct socket *sock)
+{
+	struct privsep *ps = env->gotwebd_ps;
+
+	if (sock->conf.type == UNIX) {
+		log_debug("%s: initializing unix socket %s", __func__,
+		    sock->conf.unix_socket_name);
+		sock->fd = sockets_unix_socket_listen(ps, sock);
+		if (sock->fd == -1) {
+			log_warnx("%s: create unix socket failed", __func__);
+			return -1;
+		}
+	}
+
+	if (sock->conf.type == FCGI) {
+		log_debug("%s: initializing fcgi socket for %s", __func__,
+		    sock->conf.name);
+		sock->fd = sockets_create_socket(sock->conf.al,
+		    sock->conf.fcgi_socket_port);
+		if (sock->fd == -1) {
+			log_warnx("%s: create unix socket failed", __func__);
+			return -1;
+		}
+	}
+
+	return 0;
+}
+
+int
+sockets_unix_socket_listen(struct privsep *ps, struct socket *sock)
+{
+	struct gotwebd *env = ps->ps_env;
+	struct sockaddr_un sun;
+	struct socket *tsock;
+	int u_fd = -1;
+	mode_t old_umask, mode;
+
+	TAILQ_FOREACH(tsock, env->sockets, entry) {
+		if (strcmp(tsock->conf.unix_socket_name,
+		    sock->conf.unix_socket_name) == 0 &&
+		    tsock->fd != -1)
+			return (tsock->fd);
+	}
+
+	u_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK| SOCK_CLOEXEC, 0);
+	if (u_fd == -1) {
+		log_warn("%s: socket", __func__);
+		return -1;
+	}
+
+	sun.sun_family = AF_UNIX;
+	if (strlcpy(sun.sun_path, sock->conf.unix_socket_name,
+	    sizeof(sun.sun_path)) >= sizeof(sun.sun_path)) {
+		log_warn("%s: %s name too long", __func__,
+		    sock->conf.unix_socket_name);
+		close(u_fd);
+		return -1;
+	}
+
+	if (unlink(sock->conf.unix_socket_name) == -1) {
+		if (errno != ENOENT) {
+			log_warn("%s: unlink %s", __func__,
+			    sock->conf.unix_socket_name);
+			close(u_fd);
+			return -1;
+		}
+	}
+
+	old_umask = umask(S_IXUSR|S_IXGRP|S_IWOTH|S_IROTH|S_IXOTH);
+	mode = S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP;
+
+	if (bind(u_fd, (struct sockaddr *)&sun, sizeof(sun)) == -1) {
+		log_warn("%s: bind: %s", __func__, sock->conf.unix_socket_name);
+		close(u_fd);
+		(void)umask(old_umask);
+		return -1;
+	}
+
+	(void)umask(old_umask);
+
+	if (chmod(sock->conf.unix_socket_name, mode) == -1) {
+		log_warn("%s: chmod", __func__);
+		close(u_fd);
+		(void)unlink(sock->conf.unix_socket_name);
+		return -1;
+	}
+
+	if (chown(sock->conf.unix_socket_name, ps->ps_pw->pw_uid,
+	    ps->ps_pw->pw_gid) == -1) {
+		log_warn("%s: chown", __func__);
+		close(u_fd);
+		(void)unlink(sock->conf.unix_socket_name);
+		return -1;
+	}
+
+	if (listen(u_fd, SOCKS_BACKLOG) == -1) {
+		log_warn("%s: listen", __func__);
+		return -1;
+	}
+
+	return u_fd;
+}
+
+int
+sockets_create_socket(struct addresslist *al, in_port_t port)
+{
+	struct addrinfo hints;
+	struct address *a;
+	int fd = -1, o_val = 1, flags;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_flags |= AI_PASSIVE;
+
+	TAILQ_FOREACH(a, al, entry) {
+		if (sockets_socket_af(&a->ss, port) == -1) {
+			log_warnx("%s: sockets_socket_af", __func__);
+			goto fail;
+		}
+
+		fd = socket(a->ss.ss_family, hints.ai_socktype,
+		    a->ipproto);
+			log_debug("%s: opening socket (%d) for %s", __func__,
+			    fd, a->ifname);
+
+		if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &o_val,
+		    sizeof(int)) == -1) {
+			log_warn("%s: setsockopt error", __func__);
+			return -1;
+		}
+
+		/* non-blocking */
+		flags = fcntl(fd, F_GETFL);
+		flags |= O_NONBLOCK;
+		fcntl(fd, F_SETFL, flags);
+
+		if (bind(fd, (struct sockaddr *)&a->ss, a->ss.ss_len) == -1) {
+			close(fd);
+			log_info("%s: can't bind to port %d", __func__,
+			    ntohs(port));
+			goto fail;
+		}
+
+		if (listen(fd, SOMAXCONN) == -1) {
+			log_warn("%s, unable to listen on socket", __func__);
+			goto fail;
+		}
+	}
+
+	free(a);
+	return (fd);
+fail:
+	free(a);
+	return -1;
+}
+
+int
+sockets_accept_reserve(int sockfd, struct sockaddr *addr, socklen_t *addrlen,
+    int reserve, volatile int *counter)
+{
+	int ret;
+
+	if (getdtablecount() + reserve +
+	    ((*counter + 1) * FD_NEEDED) >= getdtablesize()) {
+		log_debug("inflight fds exceeded");
+		errno = EMFILE;
+		return -1;
+	}
+
+	if ((ret = accept4(sockfd, addr, addrlen,
+	    SOCK_NONBLOCK | SOCK_CLOEXEC)) > -1) {
+		(*counter)++;
+		log_debug("inflight incremented, now %d", *counter);
+	}
+
+	return ret;
+}
+
+void
+sockets_accept_paused(int fd, short events, void *arg)
+{
+	struct socket *sock = (struct socket *)arg;
+
+	event_add(&sock->ev, NULL);
+}
+
+void
+sockets_socket_accept(int fd, short event, void *arg)
+{
+	struct socket *sock = (struct socket *)arg;
+	struct sockaddr_storage ss;
+	struct timeval backoff;
+	struct request *c = NULL;
+	socklen_t len;
+	int s;
+
+	backoff.tv_sec = 1;
+	backoff.tv_usec = 0;
+
+	event_add(&sock->ev, NULL);
+	if (event & EV_TIMEOUT)
+		return;
+
+	len = sizeof(ss);
+
+	s = sockets_accept_reserve(fd, (struct sockaddr *)&ss, &len,
+	    FD_RESERVE, &cgi_inflight);
+
+	if (s == -1) {
+		switch (errno) {
+		case EINTR:
+		case EWOULDBLOCK:
+		case ECONNABORTED:
+			return;
+		case EMFILE:
+		case ENFILE:
+			event_del(&sock->ev);
+			evtimer_add(&sock->pause, &backoff);
+			return;
+		default:
+			log_warn("%s: accept", __func__);
+		}
+	}
+
+	if (client_cnt > GOTWEBD_MAXCLIENTS)
+		goto err;
+
+	c = calloc(1, sizeof(struct request));
+	if (c == NULL) {
+		log_warn("%s", __func__);
+		close(s);
+		cgi_inflight--;
+		return;
+	}
+
+	c->fd = s;
+	c->sock = sock;
+	memcpy(c->priv_fd, sock->priv_fd, sizeof(c->priv_fd));
+	c->buf_pos = 0;
+	c->buf_len = 0;
+	c->request_started = 0;
+	c->sock->client_status = CLIENT_CONNECT;
+
+	event_set(&c->ev, s, EV_READ, fcgi_request, c);
+	event_add(&c->ev, NULL);
+
+	evtimer_set(&c->tmo, fcgi_timeout, c);
+	evtimer_add(&c->tmo, &timeout);
+
+	client_cnt++;
+
+	return;
+err:
+	cgi_inflight--;
+	close(s);
+	if (c != NULL)
+		free(c);
+}
+
+void
+sockets_rlimit(int maxfd)
+{
+	struct rlimit rl;
+
+	if (getrlimit(RLIMIT_NOFILE, &rl) == -1)
+		fatal("%s: failed to get resource limit", __func__);
+	log_debug("%s: max open files %llu", __func__, rl.rlim_max);
+
+	/*
+	 * Allow the maximum number of open file descriptors for this
+	 * login class (which should be the class "daemon" by default).
+	 */
+	if (maxfd == -1)
+		rl.rlim_cur = rl.rlim_max;
+	else
+		rl.rlim_cur = MAXIMUM(rl.rlim_max, (rlim_t)maxfd);
+	if (setrlimit(RLIMIT_NOFILE, &rl) == -1)
+		fatal("%s: failed to set resource limit", __func__);
+}
\ No newline at end of file