Commit Diff


commit - 5f564d23e9ea96fd6565ee4099fba6c31b71567e
commit + d3a08f4d172687d4e3e60e7faaa8830bb7f9f9db
blob - 4d6ae12143b2610a5d740e9e462e7c21dd25279f
blob + 10a141fc7c4652916ed4bbf9b46a753283a39bcd
--- Makefile
+++ Makefile
@@ -19,7 +19,7 @@ lex.yy.c: lex.l y.tab.c
 y.tab.c: parse.y
 	${YACC} -b y -d parse.y
 
-OBJS = gmid.o iri.o utf8.o lex.yy.o y.tab.o ex.o cgi.o sandbox.o
+OBJS = gmid.o iri.o utf8.o lex.yy.o y.tab.o ex.o server.o sandbox.o
 gmid: ${OBJS}
 	${CC} ${OBJS} -o gmid ${LDFLAGS}
 
@@ -29,8 +29,8 @@ static: ${OBJS}
 		-o gmid
 	strip gmid
 
-TAGS: gmid.c iri.c utf8.c ex.c cgi.c sandbox.c
-	-etags gmid.c iri.c utf8.c ex.c cgi.c sandbox.c || true
+TAGS: gmid.c iri.c utf8.c ex.c server.c sandbox.c
+	-etags gmid.c iri.c utf8.c ex.c server.c sandbox.c || true
 
 clean:
 	rm -f *.o lex.yy.c y.tab.c y.tab.h y.output gmid iri_test
blob - bb1254b240dfceced27ad814dcf9570fda4af6c9
blob + dad0d299000bee04694d47e700b9e2b413f16c4c
--- gmid.c
+++ gmid.c
@@ -14,12 +14,9 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#include <sys/mman.h>
-#include <sys/stat.h>
-
-#include <assert.h>
 #include <err.h>
 #include <errno.h>
+
 #include <fcntl.h>
 #include <limits.h>
 #include <netdb.h>
@@ -31,7 +28,6 @@
 
 struct vhost hosts[HOSTSLEN];
 
-int connected_clients;
 int goterror;
 
 int exfd;
@@ -60,9 +56,7 @@ struct etm {			/* file extension to mime */
 	{NULL, NULL}
 };
 
-__attribute__ ((format (printf, 1, 2)))
-__attribute__ ((__noreturn__))
-static inline void
+void
 fatal(const char *fmt, ...)
 {
 	va_list ap;
@@ -138,31 +132,6 @@ starts_with(const char *str, const char *prefix)
 	return 1;
 }
 
-int
-start_reply(struct pollfd *pfd, struct client *client, int code, const char *reason)
-{
-	char buf[1030]; 	/* status + ' ' + max reply len + \r\n\0 */
-	int len;
-
-	client->code = code;
-	client->meta = reason;
-	client->state = S_INITIALIZING;
-
-	len = snprintf(buf, sizeof(buf), "%d %s\r\n", code, reason);
-	assert(len < (int)sizeof(buf));
-
-	switch (tls_write(client->ctx, buf, len)) {
-	case TLS_WANT_POLLIN:
-		pfd->events = POLLIN;
-		return 0;
-	case TLS_WANT_POLLOUT:
-		pfd->events = POLLOUT;
-		return 0;
-	default:
-		return 1;
-	}
-}
-
 ssize_t
 filesize(int fd)
 {
@@ -207,286 +176,74 @@ mime(const char *path)
 	return def;
 }
 
-int
-check_path(struct client *c, const char *path, int *fd)
+char *
+absolutify_path(const char *path)
 {
-	struct stat sb;
+	char *wd, *r;
 
-	assert(path != NULL);
-	if ((*fd = openat(c->host->dirfd, *path ? path : ".",
-	    O_RDONLY | O_NOFOLLOW | O_CLOEXEC)) == -1) {
-		return FILE_MISSING;
-	}
+	if (*path == '/')
+		return strdup(path);
 
-	if (fstat(*fd, &sb) == -1) {
-		LOGN(c, "failed stat for %s: %s", path, strerror(errno));
-		return FILE_MISSING;
-	}
+	wd = getcwd(NULL, 0);
+	if (asprintf(&r, "%s/%s", wd, path) == -1)
+		err(1, "asprintf");
+	free(wd);
+	return r;
+}
 
-	if (S_ISDIR(sb.st_mode))
-		return FILE_DIRECTORY;
-
-	if (sb.st_mode & S_IXUSR)
-		return FILE_EXECUTABLE;
-
-	return FILE_EXISTS;
+void
+yyerror(const char *msg)
+{
+	goterror = 1;
+	fprintf(stderr, "%d: %s\n", yylineno, msg);
 }
 
 int
-open_file(char *fpath, char *query, struct pollfd *fds, struct client *c)
+parse_portno(const char *p)
 {
-	switch (check_path(c, fpath, &c->fd)) {
-	case FILE_EXECUTABLE:
-		if (c->host->cgi != NULL && starts_with(fpath, c->host->cgi))
-			return start_cgi(fpath, "", query, fds, c);
+	char *ep;
+	long lval;
 
-		/* fallthrough */
-
-	case FILE_EXISTS:
-		if ((c->len = filesize(c->fd)) == -1) {
-			LOGE(c, "failed to get file size for %s", fpath);
-			goodbye(fds, c);
-			return 0;
-		}
-
-		if ((c->buf = mmap(NULL, c->len, PROT_READ, MAP_PRIVATE,
-			    c->fd, 0)) == MAP_FAILED) {
-			warn("mmap: %s", fpath);
-			goodbye(fds, c);
-			return 0;
-		}
-		c->i = c->buf;
-		return start_reply(fds, c, SUCCESS, mime(fpath));
-
-	case FILE_DIRECTORY:
-		LOGD(c, "%s is a directory, trying %s/index.gmi", fpath, fpath);
-		close(c->fd);
-		c->fd = -1;
-		send_dir(fpath, fds, c);
-		return 0;
-
-	case FILE_MISSING:
-		if (c->host->cgi != NULL && starts_with(fpath, c->host->cgi))
-			return check_for_cgi(fpath, query, fds, c);
-
-		if (!start_reply(fds, c, NOT_FOUND, "not found"))
-			return 0;
-		goodbye(fds, c);
-		return 0;
-
-	default:
-		/* unreachable */
-		abort();
-	}
+	errno = 0;
+	lval = strtol(p, &ep, 10);
+	if (p[0] == '\0' || *ep != '\0')
+		errx(1, "not a number: %s", p);
+	if (lval < 0 || lval > UINT16_MAX)
+		errx(1, "port number out of range for domain %s: %ld", p, lval);
+	return lval;
 }
 
 void
-send_file(char *path, char *query, struct pollfd *fds, struct client *c)
+parse_conf(const char *path)
 {
-	ssize_t ret, len;
-
-	if (c->fd == -1) {
-		if (!open_file(path, query, fds, c))
-			return;
-		c->state = S_SENDING;
-	}
+	if ((yyin = fopen(path, "r")) == NULL)
+		err(1, "cannot open config %s", path);
+	yyparse();
+	fclose(yyin);
 
-	len = (c->buf + c->len) - c->i;
-
-	while (len > 0) {
-		switch (ret = tls_write(c->ctx, c->i, len)) {
-		case -1:
-			LOGE(c, "tls_write: %s", tls_error(c->ctx));
-			goodbye(fds, c);
-			return;
-
-		case TLS_WANT_POLLIN:
-			fds->events = POLLIN;
-			return;
-
-		case TLS_WANT_POLLOUT:
-			fds->events = POLLOUT;
-			return;
-
-		default:
-			c->i += ret;
-			len -= ret;
-			break;
-		}
-	}
-
-	goodbye(fds, c);
+	if (goterror)
+		exit(1);
 }
 
 void
-send_dir(char *path, struct pollfd *fds, struct client *client)
+load_vhosts(struct tls_config *tlsconf)
 {
-	char fpath[PATHBUF];
-	size_t len;
-
-	bzero(fpath, PATHBUF);
-
-	if (path[0] != '.')
-		fpath[0] = '.';
-
-	/* this cannot fail since sizeof(fpath) > maxlen of path */
-	strlcat(fpath, path, PATHBUF);
-	len = strlen(fpath);
-
-	/* add a trailing / in case. */
-	if (fpath[len-1] != '/') {
-		fpath[len] = '/';
-	}
-
-	strlcat(fpath, "index.gmi", sizeof(fpath));
-
-	send_file(fpath, NULL, fds, client);
-}
-
-void
-handle_handshake(struct pollfd *fds, struct client *c)
-{
 	struct vhost *h;
-	const char *servname;
 
-	switch (tls_handshake(c->ctx)) {
-	case 0:  /* success */
-	case -1: /* already handshaked */
-		break;
-	case TLS_WANT_POLLIN:
-		fds->events = POLLIN;
-		return;
-	case TLS_WANT_POLLOUT:
-		fds->events = POLLOUT;
-		return;
-	default:
-		/* unreachable */
-		abort();
-	}
+	/* we need to set something, then we can add how many key we want */
+	if (tls_config_set_keypair_file(tlsconf, hosts->cert, hosts->key))
+		errx(1, "tls_config_set_keypair_file failed");
 
-	servname = tls_conn_servername(c->ctx);
-	if (servname == NULL)
-		goto hostnotfound;
-
 	for (h = hosts; h->domain != NULL; ++h) {
-		if (!strcmp(h->domain, servname) || !strcmp(h->domain, "*"))
-			break;
-	}
-
-	if (h->domain != NULL) {
-		c->state = S_OPEN;
-		c->host = h;
-		handle_open_conn(fds, c);
-		return;
-	}
-
-hostnotfound:
-	/* XXX: check the correct response */
-	if (!start_reply(fds, c, BAD_REQUEST, "Wrong host or missing SNI"))
-		return;
-	goodbye(fds, c);
-}
-
-void
-handle_open_conn(struct pollfd *fds, struct client *c)
-{
-	char buf[GEMINI_URL_LEN];
-	const char *parse_err = "invalid request";
-	struct iri iri;
-
-	bzero(buf, sizeof(buf));
-
-	switch (tls_read(c->ctx, buf, sizeof(buf)-1)) {
-	case -1:
-		LOGE(c, "tls_read: %s", tls_error(c->ctx));
-		goodbye(fds, c);
-		return;
-
-	case TLS_WANT_POLLIN:
-		fds->events = POLLIN;
-		return;
-
-	case TLS_WANT_POLLOUT:
-		fds->events = POLLOUT;
-		return;
-	}
-
-	if (!trim_req_iri(buf) || !parse_iri(buf, &iri, &parse_err)) {
-		if (!start_reply(fds, c, BAD_REQUEST, parse_err))
-			return;
-		goodbye(fds, c);
-		return;
-	}
+		if (tls_config_add_keypair_file(tlsconf, h->cert, h->key) == -1)
+			errx(1, "failed to load the keypair (%s, %s)",
+			    h->cert, h->key);
 
-	if (strcmp(iri.schema, "gemini") || iri.port_no != conf.port) {
-		if (!start_reply(fds, c, PROXY_REFUSED, "won't proxy request"))
-			return;
-		goodbye(fds, c);
-		return;
+		if ((h->dirfd = open(h->dir, O_RDONLY | O_DIRECTORY)) == -1)
+			err(1, "open %s for domain %s", h->dir, h->domain);
 	}
-
-	LOGI(c, "GET %s%s%s",
-	    *iri.path ? iri.path : "/",
-	    *iri.query ? "?" : "",
-	    *iri.query ? iri.query : "");
-
-	send_file(iri.path, iri.query, fds, c);
 }
 
-void
-handle(struct pollfd *fds, struct client *client)
-{
-	switch (client->state) {
-	case S_HANDSHAKE:
-		handle_handshake(fds, client);
-		break;
-
-	case S_OPEN:
-                handle_open_conn(fds, client);
-		break;
-
-	case S_INITIALIZING:
-		if (!start_reply(fds, client, client->code, client->meta))
-			return;
-
-		if (client->code != SUCCESS) {
-			/* we don't need a body */
-			goodbye(fds, client);
-			return;
-		}
-
-		client->state = S_SENDING;
-
-		/* fallthrough */
-
-	case S_SENDING:
-		if (client->child)
-			handle_cgi(fds, client);
-		else
-			send_file(NULL, NULL, fds, client);
-		break;
-
-	case S_CLOSING:
-		goodbye(fds, client);
-		break;
-
-	default:
-		/* unreachable */
-		abort();
-	}
-}
-
-void
-mark_nonblock(int fd)
-{
-	int flags;
-
-	if ((flags = fcntl(fd, F_GETFL)) == -1)
-		fatal("fcntl(F_GETFL): %s", strerror(errno));
-	if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
-		fatal("fcntl(F_SETFL): %s", strerror(errno));
-}
-
 int
 make_socket(int port, int family)
 {
@@ -542,198 +299,7 @@ make_socket(int port, int family)
 	return sock;
 }
 
-void
-do_accept(int sock, struct tls *ctx, struct pollfd *fds, struct client *clients)
-{
-	int i, fd;
-	struct sockaddr_storage addr;
-	socklen_t len;
-
-	len = sizeof(addr);
-	if ((fd = accept(sock, (struct sockaddr*)&addr, &len)) == -1) {
-		if (errno == EWOULDBLOCK)
-			return;
-		fatal("accept: %s", strerror(errno));
-	}
-
-	mark_nonblock(fd);
-
-	for (i = 0; i < MAX_USERS; ++i) {
-		if (fds[i].fd == -1) {
-			bzero(&clients[i], sizeof(struct client));
-			if (tls_accept_socket(ctx, &clients[i].ctx, fd) == -1)
-				break; /* goodbye fd! */
-
-			fds[i].fd = fd;
-			fds[i].events = POLLIN;
-
-			clients[i].state = S_HANDSHAKE;
-			clients[i].fd = -1;
-			clients[i].child = 0;
-			clients[i].waiting_on_child = 0;
-			clients[i].buf = MAP_FAILED;
-			clients[i].af = AF_INET;
-			clients[i].addr = addr;
-
-			connected_clients++;
-			return;
-		}
-	}
-
-	close(fd);
-}
-
-void
-goodbye(struct pollfd *pfd, struct client *c)
-{
-	c->state = S_CLOSING;
-
-	switch (tls_close(c->ctx)) {
-	case TLS_WANT_POLLIN:
-		pfd->events = POLLIN;
-		return;
-	case TLS_WANT_POLLOUT:
-		pfd->events = POLLOUT;
-		return;
-	}
-
-	connected_clients--;
-
-	tls_free(c->ctx);
-	c->ctx = NULL;
-
-	if (c->buf != MAP_FAILED)
-		munmap(c->buf, c->len);
-
-	if (c->fd != -1)
-		close(c->fd);
-
-	close(pfd->fd);
-	pfd->fd = -1;
-}
-
-void
-loop(struct tls *ctx, int sock4, int sock6)
-{
-	int i;
-	struct client clients[MAX_USERS];
-	struct pollfd fds[MAX_USERS];
-
-	for (i = 0; i < MAX_USERS; ++i) {
-		fds[i].fd = -1;
-		fds[i].events = POLLIN;
-		bzero(&clients[i], sizeof(struct client));
-	}
-
-	fds[0].fd = sock4;
-	fds[1].fd = sock6;
-
-	for (;;) {
-		if (poll(fds, MAX_USERS, INFTIM) == -1) {
-			if (errno == EINTR) {
-                                warnx("connected clients: %d",
-				    connected_clients);
-				continue;
-			}
-			fatal("poll: %s", strerror(errno));
-		}
-
-		for (i = 0; i < MAX_USERS; i++) {
-			if (fds[i].revents == 0)
-				continue;
-
-			if (fds[i].revents & (POLLERR|POLLNVAL))
-				fatal("bad fd %d: %s", fds[i].fd,
-				    strerror(errno));
-
-			if (fds[i].revents & POLLHUP) {
-				/* fds[i] may be the fd of the stdin
-				 * of a cgi script that has exited. */
-				if (!clients[i].waiting_on_child) {
-					goodbye(&fds[i], &clients[i]);
-					continue;
-				}
-			}
-
-			if (fds[i].fd == sock4)
-				do_accept(sock4, ctx, fds, clients);
-			else if (fds[i].fd == sock6)
-				do_accept(sock6, ctx, fds, clients);
-			else
-				handle(&fds[i], &clients[i]);
-		}
-	}
-}
-
-char *
-absolutify_path(const char *path)
-{
-	char *wd, *r;
-
-	if (*path == '/')
-		return strdup(path);
-
-	wd = getcwd(NULL, 0);
-	if (asprintf(&r, "%s/%s", wd, path) == -1)
-		err(1, "asprintf");
-	free(wd);
-	return r;
-}
-
-void
-yyerror(const char *msg)
-{
-	goterror = 1;
-	fprintf(stderr, "%d: %s\n", yylineno, msg);
-}
-
 int
-parse_portno(const char *p)
-{
-	char *ep;
-	long lval;
-
-	errno = 0;
-	lval = strtol(p, &ep, 10);
-	if (p[0] == '\0' || *ep != '\0')
-		errx(1, "not a number: %s", p);
-	if (lval < 0 || lval > UINT16_MAX)
-		errx(1, "port number out of range for domain %s: %ld", p, lval);
-	return lval;
-}
-
-void
-parse_conf(const char *path)
-{
-	if ((yyin = fopen(path, "r")) == NULL)
-		err(1, "cannot open config %s", path);
-	yyparse();
-	fclose(yyin);
-
-	if (goterror)
-		exit(1);
-}
-
-void
-load_vhosts(struct tls_config *tlsconf)
-{
-	struct vhost *h;
-
-	/* we need to set something, then we can add how many key we want */
-	if (tls_config_set_keypair_file(tlsconf, hosts->cert, hosts->key))
-		errx(1, "tls_config_set_keypair_file failed");
-
-	for (h = hosts; h->domain != NULL; ++h) {
-		if (tls_config_add_keypair_file(tlsconf, h->cert, h->key) == -1)
-			errx(1, "failed to load the keypair (%s, %s)",
-			    h->cert, h->key);
-
-		if ((h->dirfd = open(h->dir, O_RDONLY | O_DIRECTORY)) == -1)
-			err(1, "open %s for domain %s", h->dir, h->domain);
-	}
-}
-
-int
 listener_main()
 {
 	int sock4, sock6;
@@ -798,8 +364,6 @@ main(int argc, char **argv)
 	conf.ipv6 = 0;
 	conf.protos = TLS_PROTOCOL_TLSv1_2 | TLS_PROTOCOL_TLSv1_3;
 
-	connected_clients = 0;
-
 	while ((ch = getopt(argc, argv, "6C:c:d:fhK:np:x:")) != -1) {
 		switch (ch) {
 		case '6':
blob - ed2a6f023dd1d1231cc6c03d473e21739014bd55
blob + 39bf27ee95a1d47e668c03d704cf299fdb5974a9
--- gmid.h
+++ gmid.h
@@ -121,37 +121,26 @@ enum {
 };
 
 /* gmid.c */
-__attribute__ ((format (printf, 3, 4))) void logs(int, struct client*, const char*, ...);
 
+__attribute__((format (printf, 1, 2)))
+__attribute__((__noreturn__))
+void fatal(const char*, ...);
+
+__attribute__((format (printf, 3, 4)))
+void logs(int, struct client*, const char*, ...);
+
 void		 sig_handler(int);
 int		 starts_with(const char*, const char*);
-
-int		 start_reply(struct pollfd*, struct client*, int, const char*);
 ssize_t		 filesize(int);
 const char	*path_ext(const char*);
 const char	*mime(const char*);
-int		 check_path(struct client*, const char*, int*);
-int		 open_file(char*, char*, struct pollfd*, struct client*);
-void		 send_file(char*, char*, struct pollfd*, struct client*);
-void		 send_dir(char*, struct pollfd*, struct client*);
-void		 handle_handshake(struct pollfd*, struct client*);
-void		 handle_open_conn(struct pollfd*, struct client*);
-void		 handle(struct pollfd*, struct client*);
-
-void		 mark_nonblock(int);
-int		 make_soket(int);
-void		 do_accept(int, struct tls*, struct pollfd*, struct client*);
-void		 goodbye(struct pollfd*, struct client*);
-void		 loop(struct tls*, int, int);
-
 char		*absolutify_path(const char*);
 void		 yyerror(const char*);
 int		 parse_portno(const char*);
 void		 parse_conf(const char*);
 void		 load_vhosts(struct tls_config*);
-
+int		 make_soket(int);
 int		 listener_main();
-
 void		 usage(const char*);
 
 /* provided by lex/yacc */
@@ -160,6 +149,25 @@ extern int yylineno;
 extern int yyparse(void);
 extern int yylex(void);
 
+/* server.c */
+int		 check_path(struct client*, const char*, int*);
+int		 open_file(char*, char*, struct pollfd*, struct client*);
+int		 check_for_cgi(char *, char*, struct pollfd*, struct client*);
+void		 mark_nonblock(int);
+void		 handle_handshake(struct pollfd*, struct client*);
+void		 handle_open_conn(struct pollfd*, struct client*);
+int		 start_reply(struct pollfd*, struct client*, int, const char*);
+int		 start_cgi(const char*, const char*, const char*, struct pollfd*, struct client*);
+void		 send_file(char*, char*, struct pollfd*, struct client*);
+void		 send_dir(char*, struct pollfd*, struct client*);
+void		 cgi_poll_on_child(struct pollfd*, struct client*);
+void		 cgi_poll_on_client(struct pollfd*, struct client*);
+void		 handle_cgi(struct pollfd*, struct client*);
+void		 goodbye(struct pollfd*, struct client*);
+void		 do_accept(int, struct tls*, struct pollfd*, struct client*);
+void		 handle(struct pollfd*, struct client*);
+void		 loop(struct tls*, int, int);
+
 /* ex.c */
 int		 send_string(int, const char*);
 int		 recv_string(int, char**);
@@ -169,13 +177,6 @@ int		 send_fd(int, int);
 int		 recv_fd(int);
 int		 executor_main(int);
 
-/* cgi.c */
-int		 check_for_cgi(char *, char*, struct pollfd*, struct client*);
-int		 start_cgi(const char*, const char*, const char*, struct pollfd*, struct client*);
-void		 cgi_poll_on_child(struct pollfd*, struct client*);
-void		 cgi_poll_on_client(struct pollfd*, struct client*);
-void		 handle_cgi(struct pollfd*, struct client*);
-
 /* sandbox.c */
 void		 sandbox();
 
blob - /dev/null
blob + ab3cd5db9de9be6294579e0bd8d34f3c29b84796 (mode 644)
--- /dev/null
+++ server.c
@@ -0,0 +1,635 @@
+/*
+ * 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 <sys/mman.h>
+#include <sys/stat.h>
+
+#include <netdb.h>
+
+#include <assert.h>
+#include <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <string.h>
+
+#include "gmid.h"
+
+int connected_clients;
+
+int
+check_path(struct client *c, const char *path, int *fd)
+{
+	struct stat sb;
+
+	assert(path != NULL);
+	if ((*fd = openat(c->host->dirfd, *path ? path : ".",
+	    O_RDONLY | O_NOFOLLOW | O_CLOEXEC)) == -1) {
+		return FILE_MISSING;
+	}
+
+	if (fstat(*fd, &sb) == -1) {
+		LOGN(c, "failed stat for %s: %s", path, strerror(errno));
+		return FILE_MISSING;
+	}
+
+	if (S_ISDIR(sb.st_mode))
+		return FILE_DIRECTORY;
+
+	if (sb.st_mode & S_IXUSR)
+		return FILE_EXECUTABLE;
+
+	return FILE_EXISTS;
+}
+
+int
+open_file(char *fpath, char *query, struct pollfd *fds, struct client *c)
+{
+	switch (check_path(c, fpath, &c->fd)) {
+	case FILE_EXECUTABLE:
+		if (c->host->cgi != NULL && starts_with(fpath, c->host->cgi))
+			return start_cgi(fpath, "", query, fds, c);
+
+		/* fallthrough */
+
+	case FILE_EXISTS:
+		if ((c->len = filesize(c->fd)) == -1) {
+			LOGE(c, "failed to get file size for %s", fpath);
+			goodbye(fds, c);
+			return 0;
+		}
+
+		if ((c->buf = mmap(NULL, c->len, PROT_READ, MAP_PRIVATE,
+			    c->fd, 0)) == MAP_FAILED) {
+			warn("mmap: %s", fpath);
+			goodbye(fds, c);
+			return 0;
+		}
+		c->i = c->buf;
+		return start_reply(fds, c, SUCCESS, mime(fpath));
+
+	case FILE_DIRECTORY:
+		LOGD(c, "%s is a directory, trying %s/index.gmi", fpath, fpath);
+		close(c->fd);
+		c->fd = -1;
+		send_dir(fpath, fds, c);
+		return 0;
+
+	case FILE_MISSING:
+		if (c->host->cgi != NULL && starts_with(fpath, c->host->cgi))
+			return check_for_cgi(fpath, query, fds, c);
+
+		if (!start_reply(fds, c, NOT_FOUND, "not found"))
+			return 0;
+		goodbye(fds, c);
+		return 0;
+
+	default:
+		/* unreachable */
+		abort();
+	}
+}
+
+
+/*
+ * the inverse of this algorithm, i.e. starting from the start of the
+ * path + strlen(cgi), and checking if each component, should be
+ * faster.  But it's tedious to write.  This does the opposite: starts
+ * from the end and strip one component at a time, until either an
+ * executable is found or we emptied the path.
+ */
+int
+check_for_cgi(char *path, char *query, struct pollfd *fds, struct client *c)
+{
+	char *end;
+	end = strchr(path, '\0');
+
+	/* NB: assume CGI is enabled and path matches cgi */
+
+	while (end > path) {
+		/* go up one level.  UNIX paths are simple and POSIX
+		 * dirname, with its ambiguities on if the given path
+		 * is changed or not, gives me headaches. */
+		while (*end != '/')
+			end--;
+		*end = '\0';
+
+		switch (check_path(c, path, &c->fd)) {
+		case FILE_EXECUTABLE:
+			return start_cgi(path, end+1, query, fds,c);
+		case FILE_MISSING:
+			break;
+		default:
+			goto err;
+		}
+
+		*end = '/';
+		end--;
+	}
+
+err:
+	if (!start_reply(fds, c, NOT_FOUND, "not found"))
+		return 0;
+	goodbye(fds, c);
+	return 0;
+}
+
+void
+mark_nonblock(int fd)
+{
+	int flags;
+
+	if ((flags = fcntl(fd, F_GETFL)) == -1)
+		fatal("fcntl(F_GETFL): %s", strerror(errno));
+	if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
+		fatal("fcntl(F_SETFL): %s", strerror(errno));
+}
+
+void
+handle_handshake(struct pollfd *fds, struct client *c)
+{
+	struct vhost *h;
+	const char *servname;
+
+	switch (tls_handshake(c->ctx)) {
+	case 0:  /* success */
+	case -1: /* already handshaked */
+		break;
+	case TLS_WANT_POLLIN:
+		fds->events = POLLIN;
+		return;
+	case TLS_WANT_POLLOUT:
+		fds->events = POLLOUT;
+		return;
+	default:
+		/* unreachable */
+		abort();
+	}
+
+	servname = tls_conn_servername(c->ctx);
+	if (servname == NULL)
+		goto hostnotfound;
+
+	for (h = hosts; h->domain != NULL; ++h) {
+		if (!strcmp(h->domain, servname) || !strcmp(h->domain, "*"))
+			break;
+	}
+
+	if (h->domain != NULL) {
+		c->state = S_OPEN;
+		c->host = h;
+		handle_open_conn(fds, c);
+		return;
+	}
+
+hostnotfound:
+	/* XXX: check the correct response */
+	if (!start_reply(fds, c, BAD_REQUEST, "Wrong host or missing SNI"))
+		return;
+	goodbye(fds, c);
+}
+
+void
+handle_open_conn(struct pollfd *fds, struct client *c)
+{
+	char buf[GEMINI_URL_LEN];
+	const char *parse_err = "invalid request";
+	struct iri iri;
+
+	bzero(buf, sizeof(buf));
+
+	switch (tls_read(c->ctx, buf, sizeof(buf)-1)) {
+	case -1:
+		LOGE(c, "tls_read: %s", tls_error(c->ctx));
+		goodbye(fds, c);
+		return;
+
+	case TLS_WANT_POLLIN:
+		fds->events = POLLIN;
+		return;
+
+	case TLS_WANT_POLLOUT:
+		fds->events = POLLOUT;
+		return;
+	}
+
+	if (!trim_req_iri(buf) || !parse_iri(buf, &iri, &parse_err)) {
+		if (!start_reply(fds, c, BAD_REQUEST, parse_err))
+			return;
+		goodbye(fds, c);
+		return;
+	}
+
+	if (strcmp(iri.schema, "gemini") || iri.port_no != conf.port) {
+		if (!start_reply(fds, c, PROXY_REFUSED, "won't proxy request"))
+			return;
+		goodbye(fds, c);
+		return;
+	}
+
+	LOGI(c, "GET %s%s%s",
+	    *iri.path ? iri.path : "/",
+	    *iri.query ? "?" : "",
+	    *iri.query ? iri.query : "");
+
+	send_file(iri.path, iri.query, fds, c);
+}
+
+int
+start_reply(struct pollfd *pfd, struct client *client, int code, const char *reason)
+{
+	char buf[1030]; 	/* status + ' ' + max reply len + \r\n\0 */
+	int len;
+
+	client->code = code;
+	client->meta = reason;
+	client->state = S_INITIALIZING;
+
+	len = snprintf(buf, sizeof(buf), "%d %s\r\n", code, reason);
+	assert(len < (int)sizeof(buf));
+
+	switch (tls_write(client->ctx, buf, len)) {
+	case TLS_WANT_POLLIN:
+		pfd->events = POLLIN;
+		return 0;
+	case TLS_WANT_POLLOUT:
+		pfd->events = POLLOUT;
+		return 0;
+	default:
+		return 1;
+	}
+}
+
+int
+start_cgi(const char *spath, const char *relpath, const char *query,
+    struct pollfd *fds, struct client *c)
+{
+	char addr[NI_MAXHOST];
+	const char *ruser, *cissuer, *chash;
+	int e;
+
+	e = getnameinfo((struct sockaddr*)&c->addr, sizeof(c->addr),
+	    addr, sizeof(addr),
+	    NULL, 0,
+	    NI_NUMERICHOST);
+	if (e != 0)
+		goto err;
+
+	if (tls_peer_cert_provided(c->ctx)) {
+		ruser = tls_peer_cert_subject(c->ctx);
+		cissuer = tls_peer_cert_issuer(c->ctx);
+		chash = tls_peer_cert_hash(c->ctx);
+	} else {
+		ruser = NULL;
+		cissuer = NULL;
+		chash = NULL;
+	}
+
+	if (!send_string(exfd, spath)
+	    || !send_string(exfd, relpath)
+	    || !send_string(exfd, query)
+	    || !send_string(exfd, addr)
+	    || !send_string(exfd, ruser)
+	    || !send_string(exfd, cissuer)
+	    || !send_string(exfd, chash)
+	    || !send_vhost(exfd, c->host))
+		goto err;
+
+	close(c->fd);
+	if ((c->fd = recv_fd(exfd)) == -1) {
+		if (!start_reply(fds, c, TEMP_FAILURE, "internal server error"))
+			return 0;
+		goodbye(fds, c);
+		return 0;
+	}
+	c->child = 1;
+	c->state = S_SENDING;
+	cgi_poll_on_child(fds, c);
+	/* handle_cgi(fds, c); */
+	return 0;
+
+err:
+	/* fatal("cannot talk to the executor process: %s", strerror(errno)); */
+	err(1, "cannot talk to the executor process");
+}
+
+void
+send_file(char *path, char *query, struct pollfd *fds, struct client *c)
+{
+	ssize_t ret, len;
+
+	if (c->fd == -1) {
+		if (!open_file(path, query, fds, c))
+			return;
+		c->state = S_SENDING;
+	}
+
+	len = (c->buf + c->len) - c->i;
+
+	while (len > 0) {
+		switch (ret = tls_write(c->ctx, c->i, len)) {
+		case -1:
+			LOGE(c, "tls_write: %s", tls_error(c->ctx));
+			goodbye(fds, c);
+			return;
+
+		case TLS_WANT_POLLIN:
+			fds->events = POLLIN;
+			return;
+
+		case TLS_WANT_POLLOUT:
+			fds->events = POLLOUT;
+			return;
+
+		default:
+			c->i += ret;
+			len -= ret;
+			break;
+		}
+	}
+
+	goodbye(fds, c);
+}
+
+void
+send_dir(char *path, struct pollfd *fds, struct client *client)
+{
+	char fpath[PATHBUF];
+	size_t len;
+
+	bzero(fpath, PATHBUF);
+
+	if (path[0] != '.')
+		fpath[0] = '.';
+
+	/* this cannot fail since sizeof(fpath) > maxlen of path */
+	strlcat(fpath, path, PATHBUF);
+	len = strlen(fpath);
+
+	/* add a trailing / in case. */
+	if (fpath[len-1] != '/') {
+		fpath[len] = '/';
+	}
+
+	strlcat(fpath, "index.gmi", sizeof(fpath));
+
+	send_file(fpath, NULL, fds, client);
+}
+
+void
+cgi_poll_on_child(struct pollfd *fds, struct client *c)
+{
+	int fd;
+
+	if (c->waiting_on_child)
+		return;
+	c->waiting_on_child = 1;
+
+	fds->events = POLLIN;
+
+	fd = fds->fd;
+	fds->fd = c->fd;
+	c->fd = fd;
+}
+
+void
+cgi_poll_on_client(struct pollfd *fds, struct client *c)
+{
+	int fd;
+
+	if (!c->waiting_on_child)
+		return;
+	c->waiting_on_child = 0;
+
+	fd = fds->fd;
+	fds->fd = c->fd;
+	c->fd = fd;
+}
+
+void
+handle_cgi(struct pollfd *fds, struct client *c)
+{
+	ssize_t r;
+
+	/* ensure c->fd is the child and fds->fd the client */
+	cgi_poll_on_client(fds, c);
+
+	while (1) {
+		if (c->len == 0) {
+			if ((r = read(c->fd, c->sbuf, sizeof(c->sbuf))) == 0)
+				goto end;
+			if (r == -1) {
+				if (errno == EAGAIN || errno == EWOULDBLOCK) {
+					cgi_poll_on_child(fds, c);
+					return;
+				}
+                                goto end;
+			}
+			c->len = r;
+			c->off = 0;
+		}
+
+		while (c->len > 0) {
+			switch (r = tls_write(c->ctx, c->sbuf + c->off, c->len)) {
+			case -1:
+				goto end;
+
+			case TLS_WANT_POLLOUT:
+				fds->events = POLLOUT;
+				return;
+
+			case TLS_WANT_POLLIN:
+				fds->events = POLLIN;
+				return;
+
+			default:
+                                c->off += r;
+				c->len -= r;
+				break;
+			}
+		}
+	}
+
+end:
+	goodbye(fds, c);
+}
+
+void
+goodbye(struct pollfd *pfd, struct client *c)
+{
+	c->state = S_CLOSING;
+
+	switch (tls_close(c->ctx)) {
+	case TLS_WANT_POLLIN:
+		pfd->events = POLLIN;
+		return;
+	case TLS_WANT_POLLOUT:
+		pfd->events = POLLOUT;
+		return;
+	}
+
+	connected_clients--;
+
+	tls_free(c->ctx);
+	c->ctx = NULL;
+
+	if (c->buf != MAP_FAILED)
+		munmap(c->buf, c->len);
+
+	if (c->fd != -1)
+		close(c->fd);
+
+	close(pfd->fd);
+	pfd->fd = -1;
+}
+
+void
+do_accept(int sock, struct tls *ctx, struct pollfd *fds, struct client *clients)
+{
+	int i, fd;
+	struct sockaddr_storage addr;
+	socklen_t len;
+
+	len = sizeof(addr);
+	if ((fd = accept(sock, (struct sockaddr*)&addr, &len)) == -1) {
+		if (errno == EWOULDBLOCK)
+			return;
+		fatal("accept: %s", strerror(errno));
+	}
+
+	mark_nonblock(fd);
+
+	for (i = 0; i < MAX_USERS; ++i) {
+		if (fds[i].fd == -1) {
+			bzero(&clients[i], sizeof(struct client));
+			if (tls_accept_socket(ctx, &clients[i].ctx, fd) == -1)
+				break; /* goodbye fd! */
+
+			fds[i].fd = fd;
+			fds[i].events = POLLIN;
+
+			clients[i].state = S_HANDSHAKE;
+			clients[i].fd = -1;
+			clients[i].child = 0;
+			clients[i].waiting_on_child = 0;
+			clients[i].buf = MAP_FAILED;
+			clients[i].af = AF_INET;
+			clients[i].addr = addr;
+
+			connected_clients++;
+			return;
+		}
+	}
+
+	close(fd);
+}
+
+void
+handle(struct pollfd *fds, struct client *client)
+{
+	switch (client->state) {
+	case S_HANDSHAKE:
+		handle_handshake(fds, client);
+		break;
+
+	case S_OPEN:
+                handle_open_conn(fds, client);
+		break;
+
+	case S_INITIALIZING:
+		if (!start_reply(fds, client, client->code, client->meta))
+			return;
+
+		if (client->code != SUCCESS) {
+			/* we don't need a body */
+			goodbye(fds, client);
+			return;
+		}
+
+		client->state = S_SENDING;
+
+		/* fallthrough */
+
+	case S_SENDING:
+		if (client->child)
+			handle_cgi(fds, client);
+		else
+			send_file(NULL, NULL, fds, client);
+		break;
+
+	case S_CLOSING:
+		goodbye(fds, client);
+		break;
+
+	default:
+		/* unreachable */
+		abort();
+	}
+}
+
+void
+loop(struct tls *ctx, int sock4, int sock6)
+{
+	int i;
+	struct client clients[MAX_USERS];
+	struct pollfd fds[MAX_USERS];
+
+	connected_clients = 0;
+
+	for (i = 0; i < MAX_USERS; ++i) {
+		fds[i].fd = -1;
+		fds[i].events = POLLIN;
+		bzero(&clients[i], sizeof(struct client));
+	}
+
+	fds[0].fd = sock4;
+	fds[1].fd = sock6;
+
+	for (;;) {
+		if (poll(fds, MAX_USERS, INFTIM) == -1) {
+			if (errno == EINTR) {
+                                warnx("connected clients: %d",
+				    connected_clients);
+				continue;
+			}
+			fatal("poll: %s", strerror(errno));
+		}
+
+		for (i = 0; i < MAX_USERS; i++) {
+			if (fds[i].revents == 0)
+				continue;
+
+			if (fds[i].revents & (POLLERR|POLLNVAL))
+				fatal("bad fd %d: %s", fds[i].fd,
+				    strerror(errno));
+
+			if (fds[i].revents & POLLHUP) {
+				/* fds[i] may be the fd of the stdin
+				 * of a cgi script that has exited. */
+				if (!clients[i].waiting_on_child) {
+					goodbye(&fds[i], &clients[i]);
+					continue;
+				}
+			}
+
+			if (fds[i].fd == sock4)
+				do_accept(sock4, ctx, fds, clients);
+			else if (fds[i].fd == sock6)
+				do_accept(sock6, ctx, fds, clients);
+			else
+				handle(&fds[i], &clients[i]);
+		}
+	}
+}