Commit Diff


commit - 50310aff335912edde625a5cde3729e34783fd7c
commit + 8ad1c570242cd93f0802931621b49b2510b338e7
blob - 16f3bbc22db226c20300675bf27b572ef3799d15
blob + 521c5b2b4b0603cebeffbf3e66f11a72e61a93ce
--- Makefile
+++ Makefile
@@ -14,7 +14,7 @@ y.tab.c: parse.y
 	${YACC} -b y -d parse.y
 
 SRCS = gmid.c iri.c utf8.c ex.c server.c sandbox.c mime.c puny.c \
-	utils.c log.c dirs.c
+	utils.c log.c dirs.c fcgi.c
 OBJS = ${SRCS:.c=.o} lex.yy.o y.tab.o ${COMPAT}
 
 gmid: ${OBJS}
blob - 6a5effe3c8d324d6ae4d3ef0af562482f5c53a95
blob + 0dcad61216f0b57fcabd1836f1eeda0d377d0537
--- ex.c
+++ ex.c
@@ -16,6 +16,8 @@
 
 #include "gmid.h"
 
+#include <sys/un.h>
+
 #include <err.h>
 #include <errno.h>
 
@@ -28,10 +30,12 @@
 #include <string.h>
 
 static void	handle_imsg_cgi_req(struct imsgbuf*, struct imsg*, size_t);
+static void	handle_imsg_fcgi_req(struct imsgbuf*, struct imsg*, size_t);
 static void	handle_imsg_quit(struct imsgbuf*, struct imsg*, size_t);
 static void	handle_dispatch_imsg(int, short, void*);
 
 static imsg_handlerfn *handlers[] = {
+	[IMSG_FCGI_REQ] = handle_imsg_fcgi_req,
 	[IMSG_CGI_REQ] = handle_imsg_cgi_req,
 	[IMSG_QUIT] = handle_imsg_quit,
 };
@@ -292,9 +296,122 @@ handle_imsg_cgi_req(struct imsgbuf *ibuf, struct imsg 
 	fd = launch_cgi(&iri, &req, h, l);
 	imsg_compose(ibuf, IMSG_CGI_RES, imsg->hdr.peerid, 0, fd, NULL, 0);
 	imsg_flush(ibuf);
+}
+
+static int
+fcgi_open_prog(struct fcgi *f)
+{
+	int s[2];
+	pid_t p;
+
+	/* XXX! */
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNIX, s) == -1)
+		err(1, "socketpair");
+
+	switch (p = fork()) {
+	case -1:
+		err(1, "fork");
+	case 0:
+		close(s[0]);
+		if (dup2(s[1], 0) == -1)
+			err(1, "dup2");
+		execl(f->prog, f->prog, NULL);
+		err(1, "execl %s", f->prog);
+	default:
+		close(s[1]);
+		return s[0];
+	}
 }
 
+static int
+fcgi_open_sock(struct fcgi *f)
+{
+	struct sockaddr_un	addr;
+	int			fd;
+
+	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
+		log_err(NULL, "socket: %s", strerror(errno));
+		return -1;
+	}
+
+	memset(&addr, 0, sizeof(addr));
+	addr.sun_family = AF_UNIX;
+	strlcpy(addr.sun_path, f->path, sizeof(addr.sun_path));
+
+	if (connect(fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
+		log_warn(NULL, "failed to connect to %s: %s", f->path,
+		    strerror(errno));
+		close(fd);
+		return -1;
+	}
+
+	return fd;
+}
+
+static int
+fcgi_open_conn(struct fcgi *f)
+{
+	struct addrinfo	hints, *servinfo, *p;
+	int		r, sock;
+
+	memset(&hints, 0, sizeof(hints));
+	hints.ai_family = AF_UNSPEC;
+	hints.ai_socktype = SOCK_STREAM;
+	hints.ai_flags = AI_ADDRCONFIG;
+
+	if ((r = getaddrinfo(f->path, f->port, &hints, &servinfo)) != 0) {
+		log_warn(NULL, "getaddrinfo %s:%s: %s", f->path, f->port,
+		    gai_strerror(r));
+		return -1;
+	}
+
+	for (p = servinfo; p != NULL; p = p->ai_next) {
+		sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol);
+		if (sock == -1)
+			continue;
+		if (connect(sock, p->ai_addr, p->ai_addrlen) == -1) {
+			close(sock);
+			continue;
+		}
+		break;
+	}
+
+	if (p == NULL) {
+		log_warn(NULL, "couldn't connect to %s:%s", f->path, f->port);
+		sock = -1;
+	}
+
+	freeaddrinfo(servinfo);
+	return sock;
+}
+
 static void
+handle_imsg_fcgi_req(struct imsgbuf *ibuf, struct imsg *imsg, size_t datalen)
+{
+	struct fcgi	*f;
+	int		 id, fd;
+
+	if (datalen != sizeof(id))
+		abort();
+	memcpy(&id, imsg->data, datalen);
+
+	if (id > FCGI_MAX || (fcgi[id].path == NULL && fcgi[id].prog == NULL))
+		abort();
+
+	f = &fcgi[id];
+	if (f->prog != NULL)
+		fd = fcgi_open_prog(f);
+	else if (f->port != NULL)
+		fd = fcgi_open_conn(f);
+	else
+		fd = fcgi_open_sock(f);
+
+	imsg_compose(ibuf, IMSG_FCGI_FD, id, 0, fd, NULL, 0);
+	imsg_flush(ibuf);
+}
+
+static void
 handle_imsg_quit(struct imsgbuf *ibuf, struct imsg *imsg, size_t datalen)
 {
 	int i;
blob - /dev/null
blob + fe4db2fd0a1889a164573f3ccff93276c73fc460 (mode 644)
--- /dev/null
+++ fcgi.c
@@ -0,0 +1,469 @@
+/*
+ * Copyright (c) 2021 Omar Polo <op@omarpolo.com>
+ *
+ * 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 "gmid.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <string.h>
+
+/*
+ * Sometimes it can be useful to inspect the fastcgi traffic as
+ * received by gmid.
+ *
+ * This will make gmid connect to a `debug.sock' socket (that must
+ * exists) in the current directory and send there a copy of what gets
+ * read.  The socket can be created and monitored e.g. with
+ *
+ *	rm -f debug.sock ; nc -Ulk ./debug.sock | hexdump -C
+ *
+ * NB: the sandbox must be disabled for this to work.
+ */
+#define DEBUG_FCGI 0
+
+#ifdef DEBUG_FCGI
+# include <sys/un.h>
+static int debug_socket = -1;
+#endif
+
+struct fcgi_header {
+	unsigned char version;
+	unsigned char type;
+	unsigned char req_id1;
+	unsigned char req_id0;
+	unsigned char content_len1;
+	unsigned char content_len0;
+	unsigned char padding;
+	unsigned char reserved;
+};
+
+/*
+ * number of bytes in a FCGI_HEADER.  Future version of the protocol
+ * will not reduce this number.
+ */
+#define FCGI_HEADER_LEN	8
+
+/*
+ * values for the version component
+ */
+#define FCGI_VERSION_1	1
+
+/*
+ * values for the type component
+ */
+#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)
+
+struct fcgi_begin_req {
+	unsigned char role1;
+	unsigned char role0;
+	unsigned char flags;
+	unsigned char reserved[5];
+};
+
+struct fcgi_begin_req_record {
+	struct fcgi_header	header;
+	struct fcgi_begin_req	body;
+};
+
+/*
+ * mask for flags;
+ */
+#define FCGI_KEEP_CONN		1
+
+/*
+ * values for the role
+ */
+#define FCGI_RESPONDER	1
+#define FCGI_AUTHORIZER	2
+#define FCGI_FILTER	3
+
+struct fcgi_end_req_body {
+	unsigned char app_status3;
+	unsigned char app_status2;
+	unsigned char app_status1;
+	unsigned char app_status0;
+	unsigned char proto_status;
+	unsigned char reserved[3];
+};
+
+/*
+ * values for proto_status
+ */
+#define FCGI_REQUEST_COMPLETE	0
+#define FCGI_CANT_MPX_CONN	1
+#define FCGI_OVERLOADED		2
+#define FCGI_UNKNOWN_ROLE	3
+
+/*
+ * Variable names for FCGI_GET_VALUES / FCGI_GET_VALUES_RESULT
+ * records.
+ */
+#define FCGI_MAX_CONNS	"FCGI_MAX_CONNS"
+#define FCGI_MAX_REQS	"FCGI_MAX_REQS"
+#define FCGI_MPXS_CONNS	"FCGI_MPXS_CONNS"
+
+static int
+prepare_header(struct fcgi_header *h, int type, int id, size_t size,
+    size_t padding)
+{
+	memset(h, 0, sizeof(*h));
+
+	/*
+	 * id=0 is reserved for status messages.
+	 */
+	id++;
+
+	h->version = FCGI_VERSION_1;
+        h->type = type;
+	h->req_id1 = (id >> 8);
+	h->req_id0 = (id & 0xFF);
+	h->content_len1 = (size >> 8);
+	h->content_len0 = (size & 0xFF);
+	h->padding = padding;
+
+	return 0;
+}
+
+static int
+fcgi_begin_request(int sock, int id)
+{
+	struct fcgi_begin_req_record r;
+
+	if (id > UINT16_MAX)
+		return -1;
+
+	memset(&r, 0, sizeof(r));
+	prepare_header(&r.header, FCGI_BEGIN_REQUEST, id,
+	    sizeof(r.body), 0);
+	assert(sizeof(r.body) == FCGI_HEADER_LEN);
+
+	r.body.role1 = 0;
+	r.body.role0 = FCGI_RESPONDER;
+	r.body.flags = FCGI_KEEP_CONN;
+
+        if (write(sock, &r, sizeof(r)) != sizeof(r))
+		return -1;
+	return 0;
+}
+
+static int
+fcgi_send_param(int sock, int id, const char *name, const char *value)
+{
+	struct fcgi_header	h;
+        uint32_t		namlen, vallen, padlen;
+	uint8_t			s[8];
+	size_t			size;
+	char			padding[8] = { 0 };
+
+	namlen = strlen(name);
+	vallen = strlen(value);
+	size = namlen + vallen + 8; /* 4 for the sizes */
+	padlen = (8 - (size & 0x7)) & 0x7;
+
+	s[0] = ( namlen >> 24)         | 0x80;
+	s[1] = ((namlen >> 16) & 0xFF);
+	s[2] = ((namlen >>  8) & 0xFF);
+	s[3] = ( namlen        & 0xFF);
+
+	s[4] = ( vallen >> 24)         | 0x80;
+	s[5] = ((vallen >> 16) & 0xFF);
+	s[6] = ((vallen >>  8) & 0xFF);
+	s[7] = ( vallen        & 0xFF);
+
+	prepare_header(&h, FCGI_PARAMS, id, size, padlen);
+
+	if (write(sock, &h, sizeof(h))   != sizeof(h) ||
+	    write(sock, s, sizeof(s))    != sizeof(s) ||
+	    write(sock, name, namlen)    != namlen    ||
+	    write(sock, value, vallen)   != vallen    ||
+	    write(sock, padding, padlen) != padlen)
+		return -1;
+
+	return 0;
+}
+
+static int
+fcgi_end_param(int sock, int id)
+{
+	struct fcgi_header h;
+
+	prepare_header(&h, FCGI_PARAMS, id, 0, 0);
+	if (write(sock, &h, sizeof(h)) != sizeof(h))
+		return -1;
+
+	prepare_header(&h, FCGI_STDIN, id, 0, 0);
+	if (write(sock, &h, sizeof(h)) != sizeof(h))
+		return -1;
+
+	return 0;
+}
+
+static int
+fcgi_abort_request(int sock, int id)
+{
+	struct fcgi_header h;
+
+	prepare_header(&h, FCGI_ABORT_REQUEST, id, 0, 0);
+	if (write(sock, &h, sizeof(h)) != sizeof(h))
+		return -1;
+
+	return 0;
+}
+
+static int
+must_read(int sock, char *d, size_t len)
+{
+	ssize_t r;
+
+#if DEBUG_FCGI
+	if (debug_socket == -1) {
+		struct sockaddr_un addr;
+
+		if ((debug_socket = socket(AF_UNIX, SOCK_STREAM, 0)) == -1)
+			err(1, "socket");
+
+		memset(&addr, 0, sizeof(addr));
+		addr.sun_family = AF_UNIX;
+		strlcpy(addr.sun_path, "./debug.sock", sizeof(addr.sun_path));
+		if (connect(debug_socket, (struct sockaddr*)&addr, sizeof(addr))
+		    == -1)
+			err(1, "connect");
+	}
+#endif
+
+	for (;;) {
+		switch (r = read(sock, d, len)) {
+		case -1:
+		case 0:
+			return -1;
+		default:
+#if DEBUG_FCGI
+			write(debug_socket, d, r);
+#endif
+
+			if (r == (ssize_t)len)
+				return 0;
+			len -= r;
+			d += r;
+		}
+	}
+}
+
+static int
+fcgi_read_header(int sock, struct fcgi_header *h)
+{
+	if (must_read(sock, (char*)h, sizeof(*h)) == -1)
+		return -1;
+	if (h->version != FCGI_VERSION_1) {
+		errno = EINVAL;
+		return -1;
+	}
+	return 0;
+}
+
+static inline int
+recid(struct fcgi_header *h)
+{
+	return h->req_id0 + (h->req_id1 << 8) - 1;
+}
+
+static inline int
+reclen(struct fcgi_header *h)
+{
+	return h->content_len0 + (h->content_len1 << 8);
+}
+
+static void
+copy_mbuf(int fd, short ev, void *d)
+{
+	struct client	*c = d;
+	struct mbuf	*mbuf;
+	size_t		 len;
+	ssize_t		 r;
+	char		*data;
+
+        for (;;) {
+		mbuf = TAILQ_FIRST(&c->mbufhead);
+		if (mbuf == NULL)
+			break;
+
+		len = mbuf->len - mbuf->off;
+		data = mbuf->data + mbuf->off;
+		switch (r = tls_write(c->ctx, data, len)) {
+		case -1:
+			/*
+			 * Can't close_conn here.  The application
+			 * needs to be informed first, otherwise it
+			 * can interfere with future connections.
+			 * Check also that we're not doing recursion
+			 * (copy_mbuf -> handle_fcgi -> copy_mbuf ...)
+			 */
+			if (c->next != NULL)
+				goto end;
+			fcgi_abort_request(0, c->id);
+			return;
+		case TLS_WANT_POLLIN:
+			event_once(c->fd, EV_READ, &copy_mbuf, c, NULL);
+			return;
+		case TLS_WANT_POLLOUT:
+			event_once(c->fd, EV_WRITE, &copy_mbuf, c, NULL);
+			return;
+		}
+		mbuf->off += r;
+
+		if (mbuf->off == mbuf->len) {
+			TAILQ_REMOVE(&c->mbufhead, mbuf, mbufs);
+			free(mbuf);
+		}
+	}
+
+end:
+	if (c->next != NULL)
+		c->next(0, 0, c);
+}
+
+static int
+consume(int fd, size_t len)
+{
+	size_t	l;
+	char	buf[64];
+
+	while (len != 0) {
+		if ((l = len) > sizeof(buf))
+			l =  sizeof(buf);
+		if (must_read(fd, buf, l) == -1)
+                        return 0;
+		len -= l;
+	}
+
+	return 1;
+}
+
+static void
+close_all(struct fcgi *f)
+{
+	size_t i;
+	struct client *c;
+
+	for (i = 0; i < MAX_USERS; i++) {
+		c = &clients[i];
+
+		if (c->fcgi != f->id)
+			continue;
+
+		if (c->code != 0)
+			close_conn(0, 0, c);
+		else
+			start_reply(c, CGI_ERROR, "CGI error");
+	}
+
+	event_del(&f->e);
+	close(f->fd);
+	f->fd = -1;
+	f->s = FCGI_OFF;
+}
+
+void
+handle_fcgi(int sock, short event, void *d)
+{
+	struct fcgi		*f = d;
+	struct fcgi_header	 h;
+	struct fcgi_end_req_body end;
+	struct client		*c;
+	struct mbuf		*mbuf;
+	size_t			 len;
+
+	if (fcgi_read_header(sock, &h) == -1)
+		goto err;
+
+	c = try_client_by_id(recid(&h));
+	if (c == NULL || c->fcgi != f->id)
+		goto err;
+
+	len = reclen(&h);
+
+	switch (h.type) {
+	case FCGI_END_REQUEST:
+		if (len != sizeof(end))
+			goto err;
+		if (must_read(sock, (char*)&end, sizeof(end)) == -1)
+			goto err;
+		/* TODO: do something with the status? */
+		c->fcgi = -1;
+		c->next = close_conn;
+		event_once(c->fd, EV_WRITE, &copy_mbuf, c, NULL);
+		break;
+
+	case FCGI_STDERR:
+		/* discard stderr (for now) */
+		if (!consume(sock, len))
+			goto err;
+		break;
+
+	case FCGI_STDOUT:
+		if ((mbuf = calloc(1, sizeof(*mbuf) + len)) == NULL)
+			fatal("calloc");
+		mbuf->len = len;
+                if (must_read(sock, mbuf->data, len) == -1) {
+			free(mbuf);
+			goto err;
+		}
+
+		if (TAILQ_EMPTY(&c->mbufhead)) {
+			TAILQ_INSERT_HEAD(&c->mbufhead, mbuf, mbufs);
+			event_once(c->fd, EV_WRITE, &copy_mbuf, c, NULL);
+		} else
+			TAILQ_INSERT_TAIL(&c->mbufhead, mbuf, mbufs);
+		break;
+
+	default:
+		log_err(NULL, "got invalid fcgi record (type=%d)", h.type);
+                goto err;
+	}
+
+	if (!consume(sock, h.padding))
+		goto err;
+	return;
+
+err:
+	close_all(f);
+}
+
+void
+send_fcgi_req(struct fcgi *f, struct client *c)
+{
+	c->next = NULL;
+
+	fcgi_begin_request(f->fd, c->id);
+	fcgi_send_param(f->fd, c->id, "QUERY_STRING", c->iri.query);
+	fcgi_send_param(f->fd, c->id, "GEMINI_URL_PATH", c->iri.path);
+	fcgi_send_param(f->fd, c->id, "SERVER_SOFTWARE", "gmid/1.7");
+	/* ... */
+
+	if (fcgi_end_param(f->fd, c->id) == -1)
+		close_all(f);
+}
blob - bdf5bd930de38dad49d7bbf63fca5282d7b6410c
blob + e04e1723b9bee94fddb8e4e6657c2ae3c8684694
--- gmid.c
+++ gmid.c
@@ -26,6 +26,8 @@
 #include <signal.h>
 #include <string.h>
 
+struct fcgi fcgi[FCGI_MAX];
+
 struct vhosthead hosts;
 
 int sock4, sock6;
@@ -251,7 +253,7 @@ free_config(void)
 	struct location *l, *tl;
 	struct envlist *e, *te;
 	struct alist *a, *ta;
-	int v;
+	int v, i;
 
 	v = conf.verbose;
 
@@ -297,6 +299,14 @@ free_config(void)
 
 		TAILQ_REMOVE(&hosts, h, vhosts);
 		free(h);
+	}
+
+	for (i = 0; i < FCGI_MAX; ++i) {
+		if (fcgi[i].path == NULL && fcgi[i].prog == NULL)
+			break;
+		free(fcgi[i].path);
+		free(fcgi[i].port);
+		free(fcgi[i].prog);
 	}
 
 	tls_free(ctx);
blob - 752653af7cf79cd1b7e9980f5c2894d1fed12828
blob + c97fd23cdc5e9b408464369fe25de5c7fbbb40b7
--- gmid.h
+++ gmid.h
@@ -26,6 +26,7 @@
 #include <netinet/in.h>
 
 #include <dirent.h>
+#include <event.h>
 #include <limits.h>
 #include <netdb.h>
 #include <signal.h>
@@ -55,8 +56,24 @@
 #define DOMAIN_NAME_LEN	(253+1)
 #define LABEL_LEN	(63+1)
 
+#define FCGI_MAX	32
 #define PROC_MAX	16
 
+struct fcgi {
+	int		 id;
+	char		*path;
+	char		*port;
+	char		*prog;
+	int		 fd;
+	struct event	 e;
+
+#define FCGI_OFF	0
+#define FCGI_INFLIGHT	1
+#define FCGI_READY	2
+	int		 s;
+};
+extern struct fcgi fcgi[FCGI_MAX];
+
 TAILQ_HEAD(lochead, location);
 struct location {
 	const char	*match;
@@ -69,6 +86,7 @@ struct location {
 	int		 strip;
 	X509_STORE	*reqca;
 	int		 disable_log;
+	int		 fcgi;
 
 	const char	*dir;
 	int		 dirfd;
@@ -157,6 +175,14 @@ struct parser {
 	const char	*err;
 };
 
+struct mbuf {
+	size_t			len;
+	size_t			off;
+	TAILQ_ENTRY(mbuf)	mbufs;
+	char			data[];
+};
+TAILQ_HEAD(mbufhead, mbuf);
+
 struct client;
 
 typedef void (imsg_handlerfn)(struct imsgbuf*, struct imsg*, size_t);
@@ -171,6 +197,7 @@ typedef void (*statefn)(int, short, void*);
  * handle_handshake -> close_conn		// on err
  *
  * handle_open_conn -> handle_cgi_reply		// via open_file/dir/...
+ * handle_open_conn -> send_fcgi_req		// via apply_fastcgi, IMSG_FCGI_FD
  * handle_open_conn -> handle_dirlist		// ...same
  * handle_open_conn -> send_file		// ...same
  * handle_open_conn -> start_reply		// on error
@@ -180,6 +207,10 @@ typedef void (*statefn)(int, short, void*);
  *
  * handle_cgi -> close_conn
  *
+ * send_fcgi_req -> copy_mbuf		// via handle_fcgi
+ * handle_fcgi -> close_all		// on error
+ * copy_mbuf -> close_conn		// on success/error
+ *
  * handle_dirlist -> send_directory_listing
  * handle_dirlist -> close_conn			// on error
  *
@@ -193,21 +224,33 @@ struct client {
 	char		 req[GEMINI_URL_LEN];
 	struct iri	 iri;
 	char		 domain[DOMAIN_NAME_LEN];
+
+	/*
+	 * start_reply uses this to know what function call after the
+	 * reply.  It's also used as sentinel value in fastcgi to know
+	 * if the server has closed the request.
+	 */
 	statefn		 next;
+
 	int		 code;
 	const char	*meta;
 	int		 fd, pfd;
 	struct dirent	**dir;
 	int		 dirlen, diroff;
+	int		 fcgi;
 
 	/* big enough to store STATUS + SPACE + META + CRLF */
 	char		 sbuf[1029];
 	ssize_t		 len, off;
 
+	struct mbufhead	 mbufhead;
+
 	struct sockaddr_storage	 addr;
 	struct vhost	*host;	/* host they're talking to */
 };
 
+extern struct client clients[MAX_USERS];
+
 struct cgireq {
 	char		buf[GEMINI_URL_LEN];
 
@@ -248,6 +291,8 @@ enum {
 enum imsg_type {
 	IMSG_CGI_REQ,
 	IMSG_CGI_RES,
+	IMSG_FCGI_REQ,
+	IMSG_FCGI_FD,
 	IMSG_LOG,
 	IMSG_QUIT,
 };
@@ -298,11 +343,16 @@ const char	*vhost_default_mime(struct vhost*, const ch
 const char	*vhost_index(struct vhost*, const char*);
 int		 vhost_auto_index(struct vhost*, const char*);
 int		 vhost_block_return(struct vhost*, const char*, int*, const char**);
+int		 vhost_fastcgi(struct vhost*, const char*);
 int		 vhost_dirfd(struct vhost*, const char*);
 int		 vhost_strip(struct vhost*, const char*);
 X509_STORE	*vhost_require_ca(struct vhost*, const char*);
 int		 vhost_disable_log(struct vhost*, const char*);
+
 void		 mark_nonblock(int);
+void		 start_reply(struct client*, int, const char*);
+void		 close_conn(int, short, void*);
+struct client	*try_client_by_id(int);
 void		 loop(struct tls*, int, int, struct imsgbuf*);
 
 /* dirs.c */
@@ -325,6 +375,10 @@ int		 send_fd(int, int);
 int		 recv_fd(int);
 int		 executor_main(struct imsgbuf*);
 
+/* fcgi.c */
+void		 handle_fcgi(int, short, void*);
+void		 send_fcgi_req(struct fcgi*, struct client*);
+
 /* sandbox.c */
 void		 sandbox_server_process(void);
 void		 sandbox_executor_process(void);
blob - 1aa87f2f23f40994f19f6cf4cc5450e93a758105
blob + eadbb188679b6f05a7cac3d47d401f6520cc3f67
--- lex.l
+++ lex.l
@@ -62,6 +62,7 @@ client		return TCLIENT;
 default		return TDEFAULT;
 entrypoint	return TENTRYPOINT;
 env		return TENV;
+fastcgi		return TFASTCGI;
 index		return TINDEX;
 ipv6		return TIPV6;
 key		return TKEY;
@@ -76,7 +77,9 @@ require		return TREQUIRE;
 return		return TRETURN;
 root		return TROOT;
 server		return TSERVER;
+spawn		return TSPAWN;
 strip		return TSTRIP;
+tcp		return TTCP;
 type		return TTYPE;
 user		return TUSER;
 
blob - 692037aa8400cbba38e7aefbfefc5981dc97cda6
blob + e3eff65b82b6d0c29ddad82ec37d1e9540549bd4
--- parse.y
+++ parse.y
@@ -47,6 +47,8 @@ int		 check_strip_no(int);
 int		 check_prefork_num(int);
 void		 advance_loc(void);
 void		 only_once(const void*, const char*);
+void		 only_oncei(int, const char*);
+int		 fastcgi_conf(char *, char *, char *);
 
 %}
 
@@ -60,7 +62,8 @@ void		 only_once(const void*, const char*);
 
 %token TIPV6 TPORT TPROTOCOLS TMIME TDEFAULT TTYPE TCHROOT TUSER TSERVER
 %token TPREFORK TLOCATION TCERT TKEY TROOT TCGI TENV TLANG TLOG TINDEX TAUTO
-%token TSTRIP TBLOCK TRETURN TENTRYPOINT TREQUIRE TCLIENT TCA TALIAS
+%token TSTRIP TBLOCK TRETURN TENTRYPOINT TREQUIRE TCLIENT TCA TALIAS TTCP
+%token TFASTCGI TSPAWN
 
 %token TERR
 
@@ -203,6 +206,29 @@ locopt		: TAUTO TINDEX TBOOL	{ loc->auto_index = $3 ? 
 			only_once(loc->default_mime, "default type");
 			loc->default_mime = $3;
 		}
+		| TFASTCGI TSPAWN TSTRING {
+			only_oncei(loc->fcgi, "fastcgi");
+			loc->fcgi = fastcgi_conf(NULL, NULL, $3);
+		}
+		| TFASTCGI TSTRING {
+			only_oncei(loc->fcgi, "fastcgi");
+			loc->fcgi = fastcgi_conf($2, NULL, NULL);
+		}
+		| TFASTCGI TTCP TSTRING TNUM {
+			char *c;
+			if (asprintf(&c, "%d", $4) == -1)
+				err(1, "asprintf");
+			only_oncei(loc->fcgi, "fastcgi");
+			loc->fcgi = fastcgi_conf($3, c, NULL);
+		}
+		| TFASTCGI TTCP TSTRING {
+			only_oncei(loc->fcgi, "fastcgi");
+			loc->fcgi = fastcgi_conf($3, xstrdup("9000"), NULL);
+		}
+		| TFASTCGI TTCP TSTRING TSTRING {
+			only_oncei(loc->fcgi, "fastcgi");
+			loc->fcgi = fastcgi_conf($3, $4, NULL);
+		}
 		| TINDEX TSTRING {
 			only_once(loc->index, "index");
 			loc->index = $2;
@@ -241,6 +267,7 @@ new_location(void)
 
 	l = xcalloc(1, sizeof(*l));
 	l->dirfd = -1;
+	l->fcgi = -1;
 	return l;
 }
 
@@ -354,3 +381,41 @@ only_once(const void *ptr, const char *name)
 	if (ptr != NULL)
 		yyerror("`%s' specified more than once", name);
 }
+
+void
+only_oncei(int i, const char *name)
+{
+	if (i != -1)
+		yyerror("`%s' specified more than once", name);
+}
+
+int
+fastcgi_conf(char *path, char *port, char *prog)
+{
+	struct fcgi	*f;
+	int		i;
+
+	for (i = 0; i < FCGI_MAX; ++i) {
+		f = &fcgi[i];
+		
+		if (f->path == NULL) {
+			f->id = i;
+			f->path = path;
+			f->port = port;
+			f->prog = prog;
+			return i;
+		}
+
+		/* XXX: what to do with prog? */
+		if (!strcmp(f->path, path) &&
+		    ((port == NULL && f->port == NULL) ||
+		     !strcmp(f->port, port))) {
+			free(path);
+			free(port);
+			return i;
+		}
+	}
+
+	yyerror("too much `fastcgi' rules defined.");
+	return -1;
+}
blob - 4e107392d2ac379331c4a93820e852aa076e3d64
blob + d2236d7ff967a489d0306a956a1e79dddedc95f5
--- sandbox.c
+++ sandbox.c
@@ -304,6 +304,8 @@ sandbox_executor_process(void)
 {
 	struct vhost	*h;
 	struct location	*l;
+	struct fcgi	*f;
+	size_t		 i;
 
 	TAILQ_FOREACH(h, &hosts, vhosts) {
 		TAILQ_FOREACH(l, &h->locations, locations) {
@@ -317,8 +319,25 @@ sandbox_executor_process(void)
 		}
 	}
 
-	/* rpath to chdir into the correct directory */
-	if (pledge("stdio rpath sendfd proc exec", NULL))
+	for (i = 0; i < FCGI_MAX; i++) {
+                f = &fcgi[i];
+		if (f->path != NULL) {
+			if (unveil(f->path, "rw") == -1)
+				fatal("unveil %s", f->path);
+		}
+
+		if (f->prog != NULL) {
+			if (unveil(f->prog, "rx") == -1)
+				fatal("unveil %s", f->prog);
+		}
+	}
+
+	/*
+	 * rpath: to chdir into the correct directory
+	 * proc exec: CGI
+	 * dns inet unix: FastCGI
+	 */
+	if (pledge("stdio rpath sendfd proc exec dns inet unix", NULL))
 		err(1, "pledge");
 }
 
blob - 79c7d9cd40b139087bcbc8fd064ab1f6aaa77a7f
blob + 7bc783bb29a65885f7b95c82d0826c919527bef3
--- server.c
+++ server.c
@@ -26,7 +26,8 @@
 #include <limits.h>
 #include <string.h>
 
-static struct client	 clients[MAX_USERS];
+struct client	 clients[MAX_USERS];
+
 static struct tls	*ctx;
 
 static struct event e4, e6, imsgev, siginfo, sigusr2;
@@ -48,7 +49,6 @@ static void	 fmt_sbuf(const char*, struct client*, con
 static int	 apply_block_return(struct client*);
 static int	 apply_require_ca(struct client*);
 static void	 handle_open_conn(int, short, void*);
-static void	 start_reply(struct client*, int, const char*);
 static void	 handle_start_reply(int, short, void*);
 static size_t	 host_nth(struct vhost*);
 static void	 start_cgi(const char*, const char*, struct client*);
@@ -60,16 +60,16 @@ static int 	 read_next_dir_entry(struct client*);
 static void	 send_directory_listing(int, short, void*);
 static void	 handle_cgi_reply(int, short, void*);
 static void	 handle_copy(int, short, void*);
-static void	 close_conn(int, short, void*);
 static void	 do_accept(int, short, void*);
-struct client	*client_by_id(int);
 static void	 handle_imsg_cgi_res(struct imsgbuf*, struct imsg*, size_t);
+static void	 handle_imsg_fcgi_fd(struct imsgbuf*, struct imsg*, size_t);
 static void	 handle_imsg_quit(struct imsgbuf*, struct imsg*, size_t);
 static void	 handle_siginfo(int, short, void*);
 
 static imsg_handlerfn *handlers[] = {
 	[IMSG_QUIT] = handle_imsg_quit,
 	[IMSG_CGI_RES] = handle_imsg_cgi_res,
+	[IMSG_FCGI_FD] = handle_imsg_fcgi_fd,
 };
 
 static inline int
@@ -203,6 +203,25 @@ vhost_block_return(struct vhost *v, const char *path, 
 }
 
 int
+vhost_fastcgi(struct vhost *v, const char *path)
+{
+	struct location *loc;
+
+	if (v == NULL || path == NULL)
+		return -1;
+
+	loc = TAILQ_FIRST(&v->locations);
+	while ((loc = TAILQ_NEXT(loc, locations)) != NULL) {
+		if (loc->fcgi != -1)
+			if (matches(loc->match, path))
+				return loc->fcgi;
+	}
+
+	loc = TAILQ_FIRST(&v->locations);
+	return loc->fcgi;
+}
+
+int
 vhost_dirfd(struct vhost *v, const char *path)
 {
 	struct location *loc;
@@ -553,6 +572,37 @@ apply_block_return(struct client *c)
 	fmt_sbuf(fmt, c, path);
 
 	start_reply(c, code, c->sbuf);
+	return 1;
+}
+
+/* 1 if matching `fcgi' (and apply it), 0 otherwise */
+static int
+apply_fastcgi(struct client *c)
+{
+	int		 id;
+	struct fcgi	*f;
+
+	if ((id = vhost_fastcgi(c->host, c->iri.path)) == -1)
+		return 0;
+
+	switch ((f = &fcgi[id])->s) {
+	case FCGI_OFF:
+		f->s = FCGI_INFLIGHT;
+		log_info(c, "opening fastcgi connection for (%s,%s,%s)",
+		    f->path, f->port, f->prog);
+		imsg_compose(&exibuf, IMSG_FCGI_REQ, 0, 0, -1,
+		    &id, sizeof(id));
+		imsg_flush(&exibuf);
+                /* fallthrough */
+	case FCGI_INFLIGHT:
+                c->fcgi = id;
+		break;
+	case FCGI_READY:
+                c->fcgi = id;
+		send_fcgi_req(f, c);
+		break;
+	}
+
 	return 1;
 }
 
@@ -627,6 +677,9 @@ handle_open_conn(int fd, short ev, void *d)
 	if (apply_block_return(c))
 		return;
 
+	if (apply_fastcgi(c))
+		return;
+
 	if (c->host->entrypoint != NULL) {
 		start_cgi(c->host->entrypoint, c->iri.path, c);
 		return;
@@ -635,7 +688,7 @@ handle_open_conn(int fd, short ev, void *d)
 	open_file(c);
 }
 
-static void
+void
 start_reply(struct client *c, int code, const char *meta)
 {
 	c->code = code;
@@ -1039,10 +1092,11 @@ end:
 	close_conn(c->fd, ev, d);
 }
 
-static void
+void
 close_conn(int fd, short ev, void *d)
 {
-	struct client *c = d;
+	struct client	*c = d;
+	struct mbuf	*mbuf;
 
 	switch (tls_close(c->ctx)) {
 	case TLS_WANT_POLLIN:
@@ -1055,6 +1109,11 @@ close_conn(int fd, short ev, void *d)
 
 	connected_clients--;
 
+	while ((mbuf = TAILQ_FIRST(&c->mbufhead)) != NULL) {
+		TAILQ_REMOVE(&c->mbufhead, mbuf, mbufs);
+		free(mbuf);
+	}
+
 	tls_free(c->ctx);
 	c->ctx = NULL;
 
@@ -1101,6 +1160,7 @@ do_accept(int sock, short et, void *d)
 			c->pfd = -1;
 			c->dir = NULL;
 			c->addr = addr;
+			c->fcgi = -1;
 
 			yield_read(fd, c, &handle_handshake);
 			connected_clients++;
@@ -1111,7 +1171,7 @@ do_accept(int sock, short et, void *d)
 	close(fd);
 }
 
-struct client *
+static struct client *
 client_by_id(int id)
 {
 	if ((size_t)id > sizeof(clients)/sizeof(clients[0]))
@@ -1119,6 +1179,14 @@ client_by_id(int id)
 	return &clients[id];
 }
 
+struct client *
+try_client_by_id(int id)
+{
+	if ((size_t)id > sizeof(clients)/sizeof(clients[0]))
+                return NULL;
+	return &clients[id];
+}
+
 static void
 handle_imsg_cgi_res(struct imsgbuf *ibuf, struct imsg *imsg, size_t len)
 {
@@ -1130,6 +1198,39 @@ handle_imsg_cgi_res(struct imsgbuf *ibuf, struct imsg 
 		start_reply(c, TEMP_FAILURE, "internal server error");
 	else
 		yield_read(c->pfd, c, &handle_cgi_reply);
+}
+
+static void
+handle_imsg_fcgi_fd(struct imsgbuf *ibuf, struct imsg *imsg, size_t len)
+{
+	struct client	*c;
+	struct fcgi	*f;
+	int		 i, id;
+
+	id = imsg->hdr.peerid;
+	f = &fcgi[id];
+
+	if ((f->fd = imsg->fd) != -1) {
+		event_set(&f->e, imsg->fd, EV_READ | EV_PERSIST, &handle_fcgi,
+		    &fcgi[id]);
+		event_add(&f->e, NULL);
+	} else {
+		f->s = FCGI_OFF;
+	}
+
+	for (i = 0; i < MAX_USERS; ++i) {
+		c = &clients[i];
+		if (c->fd == -1)
+			continue;
+		if (c->fcgi != id)
+			continue;
+
+		if (f->fd == -1) {
+			c->fcgi = -1;
+			start_reply(c, TEMP_FAILURE, "internal server error");
+		} else
+			send_fcgi_req(f, c);
+	}
 }
 
 static void