Commit Diff


commit - 0f9f2d04f9c340c761a9c900fac1c225b5a05419
commit + f224a3fe688bf2ae4c108d6b4359df4d8f03db99
blob - e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
blob + 423f4babe9aaca49d8fb8f9c50a42d311cd3d01f
--- README
+++ README
@@ -0,0 +1,64 @@
+twind
+=====
+
+twind is a simple daemon serving static files over the gemini protocol.  It is
+intended to have as few knobs as possible and has no support for a
+configuration file.
+
+Installation
+------------
+
+twind is written in plain C and you need to have the following software
+installed:
+
+* A C compiler (tested with clang and GCC)
+* LibreSSL or OpenSSL
+* POSIX compatible libc with pthreads support
+* make (both BSD and GNU make will work)
+
+twind needs a dedicated user called '_twind' and directory to run.  The
+Makefile contains a command to create the user.  Note that you shall not change
+the user's name and the directory twind needs!
+
+$ make
+# make install
+# make user
+
+TLS certificates
+----------------
+
+twind expects to find a X509 certificate and a corresponding private key
+under the following locations (which cannot be changed):
+
+* /etc/twind/twind.cert.pem
+* /etc/twind/twind.key.pem
+
+Either copy your existing keys to these locations or generate a new key and
+certificate via the Makefile.  Note that the command overwrites any existing
+key without warning!  To generate both key and certificate use the following
+command and provide the hostname via the HN variable.  If you don't provide the
+hostname the command will fail!
+
+# make setuptls HN=example.com
+
+Usage
+-----
+
+twind has support for virtual hosts.  If your gemini server is called
+example.com you have to create a dedicated sub directory under /var/twind:
+
+# cd /var/twind
+# mkdir example.com
+# <copy files into the example.com directory>
+
+In case your server is also reachable via gemini.example.com and you want to
+serve the same content as on example.com you can create a symlink.  In case you
+want to serve different content, you have to create a dedicated sub directory.
+
+twind needs root permissions to start and will drop its privileges as soon as
+possible.  It will also chroot to /var/twind.
+
+# twind
+
+For debugging purposes, you can start twind with -df option so that debugging
+and running in the foreground is enabled.
blob - /dev/null
blob + 4a6e155447de055fad5fe755c7f64df9cbc580c2 (mode 644)
--- /dev/null
+++ .gitignore
@@ -0,0 +1,2 @@
+*.o
+twind
blob - /dev/null
blob + 20824e63b2355b7204e92a3d14dd59a15808a677 (mode 644)
--- /dev/null
+++ Makefile
@@ -0,0 +1,49 @@
+CC = cc
+
+#CFLAGS  = -g3 -ggdb
+CFLAGS  = -O2
+
+CFLAGS += -pipe -fPIE -fdiagnostics-color -Wno-unknown-warning-option -Wpedantic
+CFLAGS += -Wall -Werror-implicit-function-declaration -Wno-format-truncation
+CFLAGS += -Wstrict-prototypes -Wmissing-prototypes -Wmissing-declarations
+CFLAGS += -Wshadow -Wpointer-arith -Wcast-qual -Wsign-compare
+CFLAGS += -fstack-protector-strong -D_FORTIFY_SOURCE=2 -Werror=format-security
+LDADD = -Wl,-z,now -Wl,-z,relro -pie -lssl -lcrypto -lpthread
+
+BIN  = twind
+OBJS = twind.o gemini.o log.o request.o mime.o util.o
+
+INSTALL ?= install -p
+
+PREFIX ?= /usr/local
+SBIN ?= $(PREFIX)/sbin
+MAN ?= $(PREFIX)/man
+GEMINIDIR ?= /var/twind
+CONFDIR ?= /etc/twind
+
+UID = 4000
+
+all: $(BIN)
+
+install: all
+	$(INSTALL) -d -m 755 -o root $(MAN)/man8
+	$(INSTALL) -d -m 750 -o root $(CONFDIR)
+	$(INSTALL) -d -m 755 -o root $(GEMINIDIR)
+	$(INSTALL) -d -m 755 -o _twind -g _twind $(GEMINIDIR)/logs
+	$(INSTALL) -m 644 -o root twind.8 $(MAN)/man8
+	$(INSTALL) -m 755 -o root twind $(SBIN)
+
+user:
+	@useradd -d $(GEMINIDIR) -s /sbin/nologin -u $(UID) _twind
+
+setuptls:
+	@openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes -keyout $(CONFDIR)/twind.key.pem -new -subj /CN=$(HN) -out $(CONFDIR)/twind.cert.pem -addext subjectAltName=DNS:$(HN)
+
+$(BIN): $(OBJS)
+	$(CC) $(LDFLAGS) -o $@ $(OBJS) $(LDADD)
+
+.c.o:
+	$(CC) $(CFLAGS) -o $@ -c $<
+
+clean:
+	rm -f $(BIN) $(OBJS)
blob - /dev/null
blob + ea78c825a6c46942ff6e065b1e5c1d2c2d39cf2b (mode 644)
--- /dev/null
+++ gemini.c
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2021 Matthias Schmidt <xhr@giessen.ccc.de>
+ *
+ * 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 <err.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "log.h"
+#include "twind.h"
+
+static void
+generate_meta(int status_code, char *meta_response_string, const char *mime)
+{
+	switch(status_code) {
+		case STATUS_INPUT:
+			snprintf(meta_response_string, 1024, "%d Present input\r\n", status_code);
+			break;
+		case STATUS_SENSITIVE_INPUT:
+			snprintf(meta_response_string, 1024, "%d Present sensitive input\r\n",
+				status_code);
+			break;
+		case STATUS_SUCCESS:
+			if (mime == NULL)
+				/* Could not deducte mime type, so send text/gemini as default */
+				snprintf(meta_response_string, 1024, "%d text/gemini\r\n", status_code);
+			else
+				snprintf(meta_response_string, 1024, "%d %s\r\n", status_code, mime);
+			break;
+		case STATUS_REDIRECT_TEMP:
+			snprintf(meta_response_string, 1024, "%d Temporary redirect\r\n",
+				status_code);
+			break;
+		case STATUS_REDIRECT_PERM:
+			snprintf(meta_response_string, 1024, "%d Permanent redirect\r\n",
+				status_code);
+			break;
+		case STATUS_TEMP_UNAVAILABLE:
+			snprintf(meta_response_string, 1024, "%d Temporary failure\r\n",
+				status_code);
+			break;
+		case STATUS_SERVER_UNAVAILABLE:
+			snprintf(meta_response_string, 1024, "%d Server unavailable\r\n",
+				status_code);
+			break;
+		case STATUS_CGI_ERROR:
+			snprintf(meta_response_string, 1024, "%d CGI Error\r\n", status_code);
+			break;
+		case STATUS_PROXY_ERROR:
+			snprintf(meta_response_string, 1024, "%d Proxy error\r\n", status_code);
+			break;
+		case STATUS_SLOW_DOWN:
+			snprintf(meta_response_string, 1024, "%d Slow down\r\n", status_code);
+			break;
+		case STATUS_PERM_FAILURE:
+			snprintf(meta_response_string, 1024, "%d Permanent failure\r\n", status_code);
+			break;
+		case STATUS_NOT_FOUND:
+			snprintf(meta_response_string, 1024, "%d Resource not found\r\n",
+				status_code);
+			break;
+		case STATUS_GONE:
+			snprintf(meta_response_string, 1024, "%d Resource is gone\r\n", status_code);
+			break;
+		case STATUS_PROXY_REQUEST_REFUSED:
+			snprintf(meta_response_string, 1024, "%d Proxy request refused\r\n",
+				status_code);
+			break;
+		case STATUS_BAD_REQUEST:
+			snprintf(meta_response_string, 1024, "%d Bad Request\r\n", status_code);
+			break;
+		case STATUS_CLIENT_CERT_REQUIRED:
+			snprintf(meta_response_string, 1024, "%d Client Certificate Required\r\n",
+				status_code);
+			break;
+		case STATUS_CERT_NOT_AUTHORIZED:
+			snprintf(meta_response_string, 1024, "%d Certificate not authorized\r\n",
+				status_code);
+			break;
+		case STATUS_CERT_NOT_VALID:
+			snprintf(meta_response_string, 1024, "%d Certificate not valid\r\n",
+				status_code);
+			break;
+		default:
+			snprintf(meta_response_string, 1024, "%d Unkown status code\r\n",
+				status_code);
+			break;
+	}
+}
+
+int
+send_non_success_response(SSL *ssl_peer, int status_code)
+{
+	char meta[1024];
+
+	memset(meta, 0, sizeof(meta));
+
+	generate_meta(status_code, meta, NULL);
+
+	log_debug("Send non success response to client: %d", status_code);
+
+	if (SSL_write(ssl_peer, meta, strlen(meta)) <= 0) {
+		log_warn("Could not send response to client");
+		return -1;
+	}
+
+	return 0;
+}
+
+int
+send_response(SSL *ssl_peer, int status_code, const char *gemini_file_path,
+	const char *mime)
+{
+	char meta[1024];
+	char buffer[1024];
+	int fd = -1, len;
+
+	// <STATUS><SPACE><META><CR><LF>
+
+	memset(meta, 0, sizeof(meta));
+	memset(buffer, 0, sizeof(buffer));
+
+	generate_meta(status_code, meta, mime);
+
+	if (SSL_write(ssl_peer, meta, strlen(meta)) <= 0) {
+		log_warn("Could not send response to client");
+		return -1;
+	}
+
+	/* Close connection and do not send a response if status code is not
+	 * a SUCCESS code
+	 */
+	if (status_code < 30 && status_code >= 20) {
+		fd = open(gemini_file_path, O_RDONLY);
+		if (fd == -1) {
+			log_warn("Cannot open requested file");
+			goto out;
+		}
+
+		while ((len = read(fd, buffer, sizeof(buffer)-1)) > 0) {
+			if (SSL_write(ssl_peer, buffer, len) <= 0) {
+				log_warn("Could not send response to client");
+				return -1;
+			}
+		}
+	}
+
+out:
+	close(fd);
+
+	return 0;
+}
+
+int
+check_gemini_file(const char *gemini_file_path)
+{
+	struct stat sb;
+
+	if (stat(gemini_file_path, &sb) == -1) {
+		log_warn("Cannot open requested file");
+		return -1;
+	}
+
+	if ((sb.st_mode & (S_IRUSR | S_IRGRP | S_IROTH)) == 0) {
+		log_warn("Cannot read requested file");
+		return -1;
+	}
+
+	if ((sb.st_mode & S_IFMT) == S_IFDIR)
+		return 1;
+
+	return 0;
+}
blob - /dev/null
blob + eca6308b9bf18101f58c4bec2b2ef6f44b69e302 (mode 644)
--- /dev/null
+++ log.c
@@ -0,0 +1,287 @@
+/*	$OpenBSD: log.c,v 1.1 2018/07/10 16:39:54 florian Exp $	*/
+
+/*
+ * 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.
+ */
+
+#define _GNU_SOURCE
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <syslog.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <time.h>
+
+#include "log.h"
+#include "twind.h"
+
+#define MAXLOGLINE 1024
+
+static int		 debug;
+static int		 verbose;
+static int access_fd;
+static int error_fd;
+
+static const char	*log_procname;
+
+void
+log_init(int n_debug, int facility)
+{
+	extern char	*__progname;
+
+	debug = n_debug;
+	verbose = n_debug;
+	log_procinit(__progname);
+
+	if (!debug)
+		openlog(__progname, 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_ERR, "%s", strerror(saved_errno));
+	else {
+		va_start(ap, emsg);
+
+		if (asprintf(&nfmt, "%s: %s", emsg,
+		    strerror(saved_errno)) == -1) {
+			/* we tried it... */
+			vlog(LOG_ERR, emsg, ap);
+			logit(LOG_ERR, "%s", strerror(saved_errno));
+		} else {
+			vlog(LOG_ERR, 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_ERR, 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) {
+		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, "fatal in %s: %s%s%s",
+		    log_procname, s, sep, strerror(code));
+	else
+		logit(LOG_CRIT, "fatal in %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);
+}
+
+void
+open_twind_logs(void)
+{
+	if ((access_fd = open(_PATH_TWIND_ACCESS_LOG, O_WRONLY|O_APPEND|O_CREAT, 0644))
+		== -1)
+		fatalx("Cannot open access log: %s", _PATH_TWIND_ACCESS_LOG);
+
+	if ((error_fd = open(_PATH_TWIND_ERROR_LOG, O_WRONLY|O_APPEND|O_CREAT, 0644))
+		== -1)
+		fatalx("Cannot open error log: %s", _PATH_TWIND_ACCESS_LOG);
+
+	return;
+}
+
+void
+close_twind_logs(void)
+{
+	close(access_fd);
+	close(error_fd);
+}
+
+void
+log_access(const struct client_connection *cc, const char *fmt, ...)
+{
+	struct tm tm;
+	time_t t;
+
+	t = time(NULL);
+	tm = *localtime(&t);
+
+	user_log(0, "%s - - [%d/%d/%d:%d:%d:%d %s] %s", cc->client_addr,
+			tm.tm_mday, tm.tm_mon, tm.tm_year+1900,
+			tm.tm_hour, tm.tm_min, tm.tm_sec,
+			tm.tm_zone, fmt);
+}
+
+void
+log_error(const struct client_connection *cc, const char *fmt, ...)
+{
+	struct tm tm;
+	time_t t;
+
+	t = time(NULL);
+	tm = *localtime(&t);
+
+	user_log(1, "[%d/%d/%d:%d:%d:%d %s] [error] [client %s] %s",
+			tm.tm_mday, tm.tm_mon, tm.tm_year+1900,
+			tm.tm_hour, tm.tm_min, tm.tm_sec,
+			tm.tm_zone,
+			cc->client_addr,
+			fmt);
+}
+
+void
+user_log(int target, const char *fmt, ...)
+{
+	va_list ap;
+	int fd = -1;
+
+	va_start(ap, fmt);
+	if (target == 0)
+		fd = access_fd;
+	else if (target == 1)
+		fd = error_fd;
+	else {
+		log_warn("Non-existent user log target");
+		return;
+	}
+
+	vdprintf(fd, fmt, ap);
+	dprintf(fd, "\n");
+
+	va_end(ap);
+}
blob - /dev/null
blob + e6fe91961572b5c65ca952f89e27493100424118 (mode 644)
--- /dev/null
+++ log.h
@@ -0,0 +1,46 @@
+/*	$OpenBSD: log.h,v 1.1 2018/07/10 16:39:54 florian Exp $ */
+
+/*
+ * 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.
+ */
+
+#ifndef LOG_H
+#define LOG_H
+
+#include <stdarg.h>
+#include <sys/cdefs.h>
+
+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)));
+void fatal(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+void fatalx(const char *, ...)
+	    __attribute__((__format__ (printf, 1, 2)));
+
+#endif /* LOG_H */
blob - /dev/null
blob + 8c4522ed6cbfe4965516422532a0f0333feb7938 (mode 644)
--- /dev/null
+++ mime.c
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2021 Matthias Schmidt <xhr@giessen.ccc.de>
+ *
+ * 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 <stddef.h>
+#include <string.h>
+
+#include "log.h"
+#include "twind.h"
+
+struct mimetype {
+	const char *ext;
+	const char *type;
+};
+
+static const struct mimetype mime_collection[] = {
+	{ "gmi", "text/gemini" },
+	{ "gemini", "text/gemini" },
+	{ "jpeg", "image/jpeg" },
+	{ "jpg", "image/jpeg" },
+	{ "html", "text/html" },
+	{ "m4a", "audio/x-m4a" },
+	{ "md", "text/markdown" },
+	{ "mov", "video/quicktime" },
+	{ "mp3", "audio/mpeg" },
+	{ "mp4", "video/mp4" },
+	{ "mpeg", "video/mpeg" },
+	{ "mpg", "video/mpeg" },
+	{ "ogg", "audio/ogg" },
+	{ "pdf", "application/pdf" },
+	{ "png", "image/png" },
+	{ "svg", "image/svg+xml" },
+	{ "txt", "text/plain" },
+	{ "wmv", "video/x-ms-wmv" }
+};
+
+#ifndef nitems
+#define nitems(_a) (sizeof((_a)) / sizeof((_a)[0]))
+#endif
+
+char *
+get_file_extension(const char *path)
+{
+	char *p, *ext;
+
+	if (strlen(path) == 0)
+		return NULL;
+
+	if ((p = strrchr(path, '.')) == NULL)
+		return NULL;
+
+	p += 1;
+	ext = xstrdup(p);
+
+	return ext;
+}
+
+char *
+get_mime_type(const char *ext)
+{
+	char *mime = NULL;
+	size_t len;
+	long unsigned int i;
+
+	if ((len = strlen(ext)) == 0)
+		return NULL;
+
+	for (i=0; i < nitems(mime_collection); i++)
+		if (strcasecmp(ext, mime_collection[i].ext) == 0)
+			mime = xstrdup(mime_collection[i].type);
+
+	return mime;
+}
blob - /dev/null
blob + 369c09028890e6a6c4844f7f3da80e1b05dff51a (mode 644)
--- /dev/null
+++ regress/Makefile
@@ -0,0 +1,13 @@
+BASEDIR=	/var/twind/localhost/tests
+
+testdirs:
+	mkdir -p $(BASEDIR)
+	mkdir -p $(BASEDIR)/subdir
+
+testfiles:
+	echo "1e6b1c887c59a315edb7eb9a315fc84c" > $(BASEDIR)/index.gmi
+	echo "1e6b1c887c59a315edb7eb9a315fc84c" > $(BASEDIR)/subdir/index.gmi
+	echo "1e6b1c887c59a315edb7eb9a315fc84c" > $(BASEDIR)/subdir/test.gmi
+	ln -s index.gmi $(BASEDIR)/link.gmi
+
+all: testdir testfiles
blob - /dev/null
blob + f031ec696fe9106e5af00c1854415be5c01f1943 (mode 644)
--- /dev/null
+++ regress/index.gmi
@@ -0,0 +1,22 @@
+# Ramblings about stuff I do or did
+
+Hi and welcome to my website.  Actually, the first one since half a decade.  Check out some articles I wrote in the last years.  Mainly about BSD but you might find some other stuff as well.
+
+This site is a copy of my HTTP website on the same domain.  I converted all content automatically and tried my best to get rid of all webisms.  If you find errors please contact me.
+
+=> about.gmi About me
+=> bluetooth.gmi Playing Wireless Audio on OpenBSD
+=> contact.gmi How to contact me
+=> enchome.gmi Encrypted HOME directory on a second disk with OpenBSD
+=> mfs.gmi /tmp Partition on Memory Filesystem
+=> pinebookpro.gmi Install OpenBSD 6.7-current on a PineBook Pro 64
+=> sandbox.gmi How to run X Applications as another User
+=> talks.gmi Talks I gave over the years
+=> ttrss.gmi  How to set up Tiny Tiny RSS on OpenBSD
+=> u2fandssh.gmi Using a U2F/FIDO key with OpenSSH
+=> vmm.gmi Running Virtual Machines with VMM on OpenBSD
+=> wireguard.gmi Creating a Wireguard VPN on OpenBSD
+
+This gemini site is powered by vger on OpenBSD.
+
+> $Id: index.gmi,v 1.3 2020/12/25 18:49:20 cvs Exp $
blob - /dev/null
blob + f67db9ff143a79f0b3d7d637fcbe6f9c30509b1b (mode 755)
--- /dev/null
+++ regress/run_tests.sh
@@ -0,0 +1,134 @@
+#!/usr/local/bin/bash
+
+HOST=${1:-"localhost"}
+
+PORT=1965
+
+check_status()
+{
+	local _status=$1
+	local _expected=$2
+	if [ "$_status" != "$_expected" ]; then
+		echo "[-] failure. Expected $_expected and got $_status"
+	fi
+}
+
+# Expect 20
+URL=""
+echo "[+] Testing ${HOST}"
+echo "gemini://${HOST}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+}
+
+# Expect 51
+URL="reallynotexistent"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 51
+}
+
+# Expect 20
+URL="/"
+echo "[+] Testing ${HOST}${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+}
+
+# Expect 20
+URL="index.gmi"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+}
+
+# Expect 20
+URL="tests/"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+}
+
+# Expect 20
+URL="tests/link.gmi"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+	read -r line
+	check_status $line "1e6b1c887c59a315edb7eb9a315fc84c"
+}
+
+# Expect 20
+URL="tests/index.gmi"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+	read -r line
+	check_status $line "1e6b1c887c59a315edb7eb9a315fc84c"
+}
+
+# Expect 20
+URL="tests/index.gmi"
+echo "[+] Testing ${HOST}:${PORT}/${URL}"
+echo "gemini://${HOST}:${PORT}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 20
+	read -r line
+	check_status $line "1e6b1c887c59a315edb7eb9a315fc84c"
+}
+
+# Expect 51
+URL="url%20encoded"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 51
+}
+
+# Expect 51
+URL="index.gemini"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 51
+}
+
+# Expect 51
+URL="../../../../../etc/passwd"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 51
+}
+
+# Expect 51
+URL="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 51
+}
+
+# Expect 59
+URL="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 59
+}
+
+# Expect 59
+URL="index.gmi"
+LHOST="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+echo "[+] Testing ${LHOST}/${URL}"
+echo "gemini://${LHOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null | { \
+	read -r status meta
+	check_status $status 59
+}
+
blob - /dev/null
blob + 021bad4679e661ad945c55c5616c700a54c14e8d (mode 755)
--- /dev/null
+++ regress/run_tests_fast.sh
@@ -0,0 +1,87 @@
+#!//bin/sh
+
+HOST=${1:-"localhost"}
+
+PORT=1965
+
+check_status()
+{
+	local _status=$1
+	local _expected=$2
+	if [ "$_status" != "$_expected" ]; then
+		echo "[-] failure. Expected $_expected and got $_status"
+	fi
+}
+
+# Expect 20
+URL=""
+echo "[+] Testing ${HOST}"
+echo "gemini://${HOST}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 51
+URL="reallynotexistent"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 20
+URL="/"
+echo "[+] Testing ${HOST}${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 20
+URL="index.gmi"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 20
+URL="tests/"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 20
+URL="tests/link.gmi"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 20
+URL="tests/index.gmi"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 20
+URL="tests/index.gmi"
+echo "[+] Testing ${HOST}:${PORT}/${URL}"
+echo "gemini://${HOST}:${PORT}/${URL}" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 51
+URL="url%20encoded"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 51
+URL="index.gemini"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 51
+URL="../../../../../etc/passwd"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 51
+URL="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 59
+URL="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+echo "[+] Testing ${HOST}/${URL}"
+echo "gemini://${HOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+# Expect 59
+URL="index.gmi"
+LHOST="AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+echo "[+] Testing ${LHOST}/${URL}"
+echo "gemini://${LHOST}/${URL}/" | openssl s_client -connect ${HOST}:${PORT} -crlf -ign_eof -quiet 2> /dev/null
+
+
blob - /dev/null
blob + b36c758f4913c481293e9917bb755982c69e6df4 (mode 644)
--- /dev/null
+++ regress/twind.cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICpDCCAYwCCQCXPDqfUlk1aTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
+b2NhbGhvc3QwHhcNMjEwODA3MTQ1ODM0WhcNMzEwODA1MTQ1ODM0WjAUMRIwEAYD
+VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0
+z7Rl/S3VXPEnJNSF+FvO6QUvQEVFTkxgzIy46w8rv7/ADrPeGR5R9omyLISUyiEV
+G6feBtjKgdzbAOTxIoTb2s26CZvJOELpnXgtY8p+mNix9qBj6Ki+4k9LpsGPSM1F
++wtvs4WelboMysWno1Ii+krJfhftRTYvEqV1nQz5ADSj8Xpr45eMl+90z4J8JzyU
+zD4pBD56oz5zlVTU5i1K3ImfQ4aph8QmqVe/Lf2DNl1dGbYUmFVotKarjYvTa5WN
+ugfz8Qm9QPVVL8B088Y4wLNQuSEA1dh3sjaBa21/oCvTDZmXPLziovEYzjBDKvi3
+WSfziOMn2DdiOQws++LTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACno6n8YCfkw
+XujPUJu5yrc32rfO4eIedPO8ByzX7Qm8Bo3H87C5jEXzzdRICViHPllGZ7e4xE1p
+UtElH/QGdZ5H27Q6X1KFSilenSvBVpk1Fi5RlAKg4KLNKX6gaFqNHnpagfxuWJeC
+9NVoaYjwRB2qUy/FiMywX7NBjOe2MMHJ0qCdQOgNi69jBSHVCQFh88WE84UPFC0T
+BY21l3VRs84g9C9w8ED7T+z6duEhasJSGG4ieK2iST05hIFr5xACF9c02/p435n2
+ZVDePbTQQP0YfzAWIOAEUNumgORuRc+gKbQrfK8WHzV6/tGVdxMyaGl0Z7BLSoQ0
+9OvvlcAFo0w=
+-----END CERTIFICATE-----
blob - /dev/null
blob + 8b73ba8572d6bebff840b7a750cc894d80c444f7 (mode 644)
--- /dev/null
+++ regress/twind.csr.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICWTCCAUECAQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAtM+0Zf0t1VzxJyTUhfhbzukFL0BFRU5MYMyMuOsP
+K7+/wA6z3hkeUfaJsiyElMohFRun3gbYyoHc2wDk8SKE29rNugmbyThC6Z14LWPK
+fpjYsfagY+iovuJPS6bBj0jNRfsLb7OFnpW6DMrFp6NSIvpKyX4X7UU2LxKldZ0M
++QA0o/F6a+OXjJfvdM+CfCc8lMw+KQQ+eqM+c5VU1OYtStyJn0OGqYfEJqlXvy39
+gzZdXRm2FJhVaLSmq42L02uVjboH8/EJvUD1VS/AdPPGOMCzULkhANXYd7I2gWtt
+f6Ar0w2Zlzy84qLxGM4wQyr4t1kn84jjJ9g3YjkMLPvi0wIDAQABoAAwDQYJKoZI
+hvcNAQELBQADggEBADAesSaEFpLqjw/smvL4MZDq1BDbWwMlfoeI5Rw0ylhDoDNs
+yA19banjyidLMN4/QjPGziqrmI9QPYsMUzKDsCrvoOV4I6rEx2xE+TfuR1c0peFB
+CT/zdwvPfq82mbZO+oyL1dMh4Dzjv0cNg3DYU9ZH/+XC/r7YKNHiV1WPpmbF2yeW
+dMcFlHPc39fgl0Jhxh7iWAuf0jPTTH7Y1JhwtpIGaxBqFB9LDJOLGLAHT+Fkms+w
+2HIsQUCc+rXhWvxoFuO/TuN94dDs/mjQv+VgC0w22tSE7tOEjTwUUxygvHNSh0A5
+ctvSeJzP+rlhGMjwFzGkU2xc/vBpxV8W4l/OOew=
+-----END CERTIFICATE REQUEST-----
blob - /dev/null
blob + c9d09bf57c9174032d14469a60788319e3e0bed9 (mode 644)
--- /dev/null
+++ regress/twind.key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAtM+0Zf0t1VzxJyTUhfhbzukFL0BFRU5MYMyMuOsPK7+/wA6z
+3hkeUfaJsiyElMohFRun3gbYyoHc2wDk8SKE29rNugmbyThC6Z14LWPKfpjYsfag
+Y+iovuJPS6bBj0jNRfsLb7OFnpW6DMrFp6NSIvpKyX4X7UU2LxKldZ0M+QA0o/F6
+a+OXjJfvdM+CfCc8lMw+KQQ+eqM+c5VU1OYtStyJn0OGqYfEJqlXvy39gzZdXRm2
+FJhVaLSmq42L02uVjboH8/EJvUD1VS/AdPPGOMCzULkhANXYd7I2gWttf6Ar0w2Z
+lzy84qLxGM4wQyr4t1kn84jjJ9g3YjkMLPvi0wIDAQABAoIBAAZnSLtH8SSaSwwY
+2NH5zr6QMBfRTeK7eCcBd4ZhBMOG4fKaUrJt7031zkCaJQPj+LH3rcVGNs1NNhYn
+fPQxRcVHhXuuNW815+DAK+5nl1dOcHY1Bs8jAT7pYueJ+1bovCRbVLdbA0NviAxF
+7iQWu6TzekySg6RqjBW0slls+3WiBfUMePkGCGhNiJuggDSbjeBudW0qTCkb89Cq
+gGt9K3YFcz934DjydHuecZpRV5bQli2jT31voDsLyihnSrNjLM/z5W248fguz85j
+HpM2syJT+rV1ubyhhxAS9/d4I/UI9EUqQfcYbe6xq0h7jzUPPsJxLKWqntA1UUi7
++rmuXmkCgYEA5kFZBG3oeiA8DoTvBlRtwIx/sMV0IseKBNN3WfnuM4YeeBCMR599
+QemjN4xQQx2UIBJtW4NYU7/yLpeCPHKJvugYl2HqdHa6ruaIHwCXs041pv71MI6G
+5WGUKGMe4VDSAFge8sZ9a3P2KmbwtFU+ywUaDbNUyjlwyKR/iSaDdM0CgYEAyQcc
+mD1mt7Ew32AJiLeUJo24xQUtzCFiI9QkelwsCp94xa6T/Df11fIj4a+6jbWUc/lw
+x3VxPF+hW31wVLVF92QlbGvSVg+e32SEAkJBH3ysr0pT2wJwEpCneydxHocpyxE1
+jvPHQaVtIxtv4f8UPIK3NNmY92t3e08tfm4jth8CgYBj+B9UAvwaegBZNXIpx2JX
+ZSjTcQc5SnUsHzwEfrTi/eogqt6dAiv6ABxzM6JtYVw8iIOeZeplgkL9454R7JDN
+qCt1HngS1LG82i5jd3hlyyEUPkHqMRd0Y+dVmaOAo/xpVdkqAu/VRWWth0Aeq5w1
+vSNQq3m2yzWih3kv7N7KSQKBgQCQO18Dx8Ij96i2C+SrR/Ouua4hBbc3J5iPVk0Z
+0Xnz4Tk4tCoPI2NprkKaUYfK1sX9c7G8GgI1q/NMfjKTREA/4IWNRcry3mBBrY+d
+Q0YQPlZzqiOCFjysxUa08LaTjayput4vg66p5fPo5W2fu3EcfTjPXXQHyP4/5a4h
+cQqERwKBgQCD2PQHRVgMGIhxb81LpClFz7EEGOJQe4VZKZvl9CFpvetvnxDZ0XPT
+fOIREEQxKd0k7X4i9JP/9xPhvlTj2wRMhx3/FRmz56JaB8rfodOOM91/k5fzxFgv
+JzYJBh9WRUo84keHUUvDXqM/Kn6cwqgCxzmPZdnleWALY+yoG3i3kQ==
+-----END RSA PRIVATE KEY-----
blob - /dev/null
blob + 609f3685eeeb7ac318a2ac6c07916e911fab47de (mode 644)
--- /dev/null
+++ request.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2021 Matthias Schmidt <xhr@giessen.ccc.de>
+ *
+ * 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 <ctype.h>
+#include <limits.h>
+#include <string.h>
+
+#include "log.h"
+#include "twind.h"
+
+char hex_to_int(char);
+char* uridecode(const char *);
+
+/*
+ * The following two functions are from https://geekhideout.com/urlcode.shtml
+ * and provided without license restrictions
+ */
+char hex_to_int(char ch) {
+	return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10;
+}
+
+char *
+uridecode(const char *request)
+{
+	char *temp = xmalloc(strlen(request) + 1);
+	const char *p;
+	char *pt;
+
+	memset(temp, 0, strlen(request)+1);
+
+	p = request;
+	pt = temp;
+
+	while (*p) {
+		if (*p == '%') {
+			if (p[1] && p[2]) {
+				*pt++ = hex_to_int(p[1]) << 4 | hex_to_int(p[2]);
+				p += 2;
+			}
+		} else if (*p == '+') {
+			*pt++ = ' ';
+		} else {
+			*pt++ = *p;
+		}
+		p++;
+	}
+	*pt = '\0';
+
+	return temp;
+}
+
+int
+get_path_from_request(char *request, char *finalpath)
+{
+	char hostname[MAXREQLEN];
+	char localpath[MAXREQLEN];
+	char temp[MAXREQLEN];
+	char *p, *decoded_request;
+	int pos = 0, ret;
+
+	memset(hostname, 0, sizeof(hostname));
+	memset(localpath, 0, sizeof(localpath));
+	memset(temp, 0, sizeof(temp));
+
+	p = request;
+
+	if ((p = strchr(request, '\r')) == NULL) {
+		log_info("\\r missing from request, abort processing");
+		return -1;
+	}
+
+	*p = '\0';	/* Strip \r\n */
+	p = request;
+
+	if (strncmp(p, "gemini://", 9) != 0) {
+		log_info("Gemini scheme missing, abort processing");
+		return -1;
+	}
+	memmove(request, p + 9, strlen(request) + 1 - 9);
+
+	decoded_request = uridecode(request);
+
+	/* save hostname */
+	if ((p = strchr(decoded_request, '/')) != NULL)
+		snprintf(hostname, strlen(decoded_request) - strlen(p)+1, "%s",
+			decoded_request);
+	else
+		snprintf(hostname, strlen(decoded_request)+1, "%s", decoded_request);
+
+	/* Strip possible port (e.g. :1965) from hostname */
+	if ((p = strrchr(hostname, ':')) != NULL) {
+		pos = strlen(hostname) - strlen(p);
+		if (pos < 0 || pos > _POSIX_HOST_NAME_MAX)
+			fatalx("pos while shorten hostname out of range");
+		hostname[pos] = '\0';
+	}
+
+	/* Remove ../ for security reasons */
+	while ((p = strstr(decoded_request, "/..")) != NULL) {
+		memmove(decoded_request, p + 3, strlen(p) + 1 - 3);
+	}
+
+	if ((p = strchr(decoded_request, '/')) != NULL) {
+		/* Save all after the first / in localpath */
+		snprintf(localpath, strlen(decoded_request), "%s", p+1);
+		if (strlen(localpath) == 0) {
+			/*
+			 * If the request is 'example.com/', localpart will be empty. In this case
+			 * write the default to it.
+			 */
+			sprintf(localpath, "index.gmi");
+		}
+	} else {
+		/* There is no slash in the request, so assume index.gmi */
+		sprintf(localpath, "index.gmi");
+	}
+
+	/*
+	 *We do not need to take the base dir aka /var/db/gemini into account
+	 * since we already chroot() to _PATH_TWIND_CHROOT .
+	 *
+	 * Here, a string truncation could happen.  This can be implemented
+	 * better! XXX FIXME
+	 */
+	snprintf(finalpath, MAXREQLEN, "%s/%s", hostname, localpath);
+
+	/* Check if the wanted path exists and if it's a directory */
+	ret = check_gemini_file(finalpath);
+	if (ret < 0) {
+		log_debug("%s not found", finalpath);
+		free(decoded_request);
+		return -2;
+	} else if (ret == 1) {
+		log_debug("%s is a directory", finalpath);
+		/* Auto append index.gmi if destination is a directory */
+		snprintf(temp, MAXREQLEN, "%s", finalpath);
+		snprintf(finalpath, MAXREQLEN, "%s/index.gmi", temp);
+	}
+
+	log_debug("Got request for %s on server %s -> %s",
+		localpath, hostname, finalpath);
+
+	/* decoded_request is no longer used, so it can be freed */
+	free(decoded_request);
+
+	return 0;
+}
+
blob - /dev/null
blob + 6e5619c8f4d1834920ad077e2a4768faeb4e12cd (mode 644)
--- /dev/null
+++ twind.8
@@ -0,0 +1,85 @@
+.\"
+.\" Copyright (c) 2021 Matthias Schmidt
+.\"
+.\" 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 August 12, 2021
+.Dt TWIND 8
+.Os
+.Sh NAME
+.Nm twind
+.Nd Simple gemini server
+.Sh SYNOPSIS
+.Nm twind
+.Op Fl dfV
+.Op Fl p Ar port
+.Sh DESCRIPTION
+.Nm
+is a simple daemon serving static files over the gemini protocol.
+It is intended to have as few knobs as possible and has no support for
+a configuration file.
+.Pp
+The options are as follows:
+.Bl -tag -width Ds
+.It Fl d
+Enable debug log messages.
+Most useful together with
+.Fl f .
+.It Fl f
+Do not daemonize.
+If this option is specified,
+.Nm
+will run in the foreground and log to
+.Em stderr .
+.It Fl p Ar port
+Listen on Port
+.Ar port
+instead of the default 1965.
+.It Fl V
+Display the version and exit.
+.El
+.Pp
+.Nm
+listens on the any address (:: and 0.0.0.0) for both IPv4 and IPv6.
+.Pp
+.Nm
+has support for virtual hosts by default.
+To serve files for a specific host you have to place them in a sub directory
+named after the domain under
+.Pa /var/twind .
+.Sh FILES
+The following path cannot be changed, i.e. you have to name your
+TLS certificate and key file exactly as shown.
+.Pp
+.Bl -tag -width Ds -compact
+.It Pa /etc/twind/twind.cert.pem
+TLS certificate for
+.Nm
+.It Pa /etc/twind/twind.key.pem
+Private key for the certificate mentioned above.
+.It Pa /var/twind/
+Default location for the gemini (gmi) files.
+Contains one sub directory for each virtual host.
+.It Pa /var/twind/example.com/
+Subdirectory containing gemini files for the
+.Em example.com
+host.
+.El
+.Sh EXIT STATUS
+.Nm
+normally exists with 0 or with -1 if an error occurred.
+.Sh AUTHORS
+.Nm
+was written by
+.An Matthias Schmidt Aq Mt xhr@giessen.ccc.de .
blob - /dev/null
blob + b7d10f05e2c3b531b4834c9ae47b067d2326861d (mode 644)
--- /dev/null
+++ twind.c
@@ -0,0 +1,544 @@
+/*
+ * Copyright (c) 2021 Matthias Schmidt <xhr@giessen.ccc.de>
+ *
+ * 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.
+ */
+
+#define _GNU_SOURCE
+
+#include <sys/file.h>
+#include <sys/queue.h>
+#include <sys/socket.h>
+#include <sys/syslog.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include <arpa/inet.h>
+
+#include <net/if.h>
+#include <netinet/in.h>
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <netdb.h>
+#include <pthread.h>
+#include <pwd.h>
+#include <signal.h>
+#include <syslog.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#if !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) &&\
+	!defined(__DragonFly__)
+#include <grp.h>
+#endif /* __BSD__ */
+
+#include "log.h"
+#include "twind.h"
+
+#define PID_BUF_SIZE 100
+#define TWIND_USER "_twind"
+#define _PATH_TWIND_CHROOT "/var/twind"
+#define _PATH_TWIND_LOGS "/var/twind/logs"
+#define _PATH_TWIND_CERT "/etc/twind/twind.cert.pem"
+#define _PATH_TWIND_KEY "/etc/twind/twind.key.pem"
+#define _PATH_TWIND_PID_CHROOT "/var/twind/twind.pid"
+#define _PATH_TWIND_PID "twind.pid"
+
+static void organize_termination(void);
+static void open_sockets(int[2], int);
+void *get_in_addr(struct sockaddr *);
+void* main_request_handler(void*);
+int receive_gemini_request(SSL*, char *);
+int handle_incoming_connections(int, int, SSL_CTX *);
+void fork_main_process(int[2], SSL_CTX *);
+SSL_CTX* initialize_tls_context(void);
+int open_pid_file(void);
+static void drop_root(void);
+
+#if !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) &&\
+	!defined(__DragonFly__)
+void setproctitle(const char *, ...);
+void setproctitle(const char *fmt, ...) {}
+#endif /* __BSD__ */
+
+static void
+usage(void)
+{
+	extern char *__progname;
+
+	fprintf(stderr, "usage: %s [-dfv] [-p port]\n", __progname);
+	exit(-1);
+}
+
+static void
+signal_handler(int signal)
+{
+	switch (signal) {
+		case SIGINT:
+		case SIGTERM:
+			organize_termination();
+			break;
+		default:
+			fatalx("Unknown signal");
+	}
+}
+
+int
+main(int argc, char *argv[])
+{
+	SSL_CTX *sslctx = NULL;
+	int ch, fg_flag = 0, debug_flag = 0, verbose_flag = 0;
+	int tcpsock[2] = { -1, -1 }, port = 1965;
+
+	log_init(1, LOG_DAEMON);        /* Log to stderr until daemonized. */
+	log_setverbose(1);
+
+	while ((ch = getopt(argc, argv, "dfp:vV")) != -1) {
+		switch(ch) {
+			case 'd':
+				debug_flag = 1;
+				break;
+			case 'f':
+				fg_flag = 1;
+				break;
+			case 'p':
+				port = atoi(optarg);
+				break;
+			case 'v':
+				verbose_flag = 1;
+				break;
+			case 'V':
+				fprintf(stderr, "Version %s\n", VERSION);
+				exit(-1);
+			default:
+				usage();
+				break;
+		}
+	}
+
+	argc -= optind;
+	argv += optind;
+
+	if (geteuid())
+		fatalx("need root privileges");
+
+	open_pid_file();
+
+	if (signal(SIGINT, signal_handler) == SIG_ERR)
+		fatalx("signal");
+	if (signal(SIGTERM, signal_handler) == SIG_ERR)
+		fatalx("signal");
+
+	open_sockets(tcpsock, port);
+
+	sslctx = initialize_tls_context();
+
+	drop_root();
+
+	log_init(debug_flag, LOG_DAEMON);
+	log_setverbose(verbose_flag);
+
+	open_twind_logs();
+
+#ifdef __OpenBSD__
+	if (pledge("stdio inet dns proc rpath", NULL) == -1)
+		fatalx("pledge");
+#endif /* __OpenBSD__ */
+
+	fork_main_process(tcpsock, sslctx);
+
+	if (!fg_flag)
+		if (daemon(0, 0) == -1)
+			fatalx("daemonizing failed");
+
+	organize_termination();
+
+	return 0;
+}
+
+static void
+organize_termination(void)
+{
+	pid_t sub_pid;
+
+	log_debug("waiting for sub processes to terminate");
+	for (;;) {
+		sub_pid = wait(NULL);
+		if (sub_pid == -1) {
+			if (errno == ECHILD) {
+				/* All sub processes are terminated */
+				close_twind_logs();
+				log_debug("twind turns to dust");
+				exit(0);
+			} else {
+				fatalx("wait");
+			}
+		}
+	}
+}
+
+SSL_CTX*
+initialize_tls_context(void)
+{
+	SSL_CTX *sslctx;
+
+	SSL_load_error_strings();
+	OpenSSL_add_all_algorithms();
+
+	sslctx = SSL_CTX_new(TLS_method());
+	if (sslctx == NULL)
+		fatalx("Cannot initialize TLS CTX structure");
+
+	SSL_CTX_set_ecdh_auto(sslctx, 1);
+
+	/* Gemini requires TLSv1.2 minimum */
+	if (SSL_CTX_set_min_proto_version(sslctx, TLS1_2_VERSION) != 1)
+		fatalx("Cannot set minimum TLS version");
+
+	if (SSL_CTX_use_certificate_file(sslctx, _PATH_TWIND_CERT, SSL_FILETYPE_PEM)
+		!= 1)
+		fatalx("Cannot load TLS certificate %s", _PATH_TWIND_CERT);
+
+	if (SSL_CTX_use_PrivateKey_file(sslctx, _PATH_TWIND_KEY, SSL_FILETYPE_PEM)
+		!= 1)
+		fatalx("Cannot load TLS private key %s", _PATH_TWIND_KEY);
+
+	return sslctx;
+}
+
+void *
+get_in_addr(struct sockaddr *sa)
+{
+	if (sa->sa_family == AF_INET)
+		return &(((struct sockaddr_in*)sa)->sin_addr);
+
+	return &(((struct sockaddr_in6*)sa)->sin6_addr);
+}
+
+int
+handle_incoming_connections(int counter, int tcpsock, SSL_CTX *sslctx)
+{
+	struct sockaddr_storage addr;
+	struct client_connection *cc;
+	char str[INET6_ADDRSTRLEN];
+	pthread_t thread_id;
+	socklen_t len = sizeof(addr);
+	int ret, ssl_err;
+
+#ifdef __OpenBSD__
+	/* We can get rid of proc pledge here */
+	if (pledge("stdio inet dns rpath", NULL) == -1)
+		fatalx("pledge");
+#endif /* __OpenBSD__ */
+
+	memset(str, 0, sizeof(str));
+
+	while (1) {
+		ret = accept(tcpsock, (struct sockaddr *)&addr, &len);
+		if (ret < 0)
+			fatalx("Error when accepting connection");
+
+		cc = xmalloc(sizeof(struct client_connection));
+
+		inet_ntop(addr.ss_family, get_in_addr((struct sockaddr *)&addr), str, sizeof(str));
+		strlcpy(cc->client_addr, str, INET6_ADDRSTRLEN);
+		//log_info("Connection from %s", cc->client_addr);
+
+		if ((cc->ssl_peer = SSL_new(sslctx)) == NULL) {
+			log_warn("Creating new TLS structure failed");
+			free(cc);
+			close(ret);
+			continue;
+		}
+
+		if (SSL_set_fd(cc->ssl_peer, ret) == 0) {
+			log_warn("TLS cannot set file descriptor");
+			SSL_free(cc->ssl_peer);
+			free(cc);
+			close(ret);
+			continue;
+		}
+
+		ssl_err = SSL_accept(cc->ssl_peer);
+		if (ssl_err < 0) {
+			ERR_print_errors_fp(stderr);
+			log_warn("Fatal TLS error. Cannot accept TLS connection");
+			SSL_shutdown(cc->ssl_peer);
+			SSL_free(cc->ssl_peer);
+			free(cc);
+			close(ret);
+			continue;
+		} else if (ssl_err == 0) {
+			log_warn("TLS handshake not successful");
+			SSL_shutdown(cc->ssl_peer);
+			SSL_free(cc->ssl_peer);
+			free(cc);
+			close(ret);
+			continue;
+		}
+
+		log_debug("SSL connection using %s", SSL_get_cipher(cc->ssl_peer));
+
+		if (pthread_create(&thread_id, NULL, main_request_handler, ((void*)cc))
+			!= 0) {
+			log_warn("Cannot create handling thread");
+			SSL_shutdown(cc->ssl_peer);
+			SSL_free(cc->ssl_peer);
+			free(cc);
+			close(ret);
+			continue;
+		}
+
+		if (pthread_join(thread_id, NULL) != 0) {
+			log_warn("Error while joining thread");
+			SSL_shutdown(cc->ssl_peer);
+			SSL_free(cc->ssl_peer);
+			free(cc);
+			close(ret);
+			continue;
+		}
+
+		SSL_shutdown(cc->ssl_peer);
+		SSL_free(cc->ssl_peer);
+		free(cc);
+		close(ret);
+	}
+
+	return 0;
+}
+
+void
+fork_main_process(int tcpsock[2], SSL_CTX *sslctx)
+{
+	pid_t pid;
+	int i;
+
+	/* Fork two main handler processes, one for IPv4, one for IPv6 */
+	for (i=0; i < 2; i++) {
+		if (tcpsock[i] == -1)
+			continue;
+		switch (pid = fork()) {
+			case -1:
+				fatalx("Cannot fork() main IPv%d handler process", i == 0 ? 4 : 6);
+			case 0:
+				log_debug("Main IPv%d handling process started: %d", i == 0 ? 4 : 6,
+					getpid());
+				setproctitle("v%d %s", i == 0 ? 4 : 6, "handler");
+				handle_incoming_connections(i, tcpsock[i], sslctx);
+				exit(0);
+		}
+	}
+}
+
+void *
+main_request_handler(void *argp)
+{
+	struct client_connection *cc = (struct client_connection *)argp;
+	char finalpath[MAXREQLEN];
+	char temp[MAXREQLEN];
+	char request[MAXREQLEN];
+	char *ext = NULL;
+	char *mime = NULL;
+	int ret;
+
+	memset(finalpath, 0, sizeof(finalpath));
+	memset(request, 0, sizeof(request));
+	memset(temp, 0, sizeof(temp));
+
+	if (receive_gemini_request(cc->ssl_peer, request) < 0) {
+		log_warn("Receiving initial request failed");
+		return NULL;
+	}
+
+	ret = get_path_from_request(request, finalpath);
+	if (ret == -1) { /* Malformed request */
+		log_error(cc, "Malformed request");
+		send_non_success_response(cc->ssl_peer, STATUS_BAD_REQUEST);
+		return NULL;
+	} else if (ret == -2) { /* 404 */
+		log_error(cc, "Request file not found");
+		send_non_success_response(cc->ssl_peer, STATUS_NOT_FOUND);
+		return NULL;
+	}
+
+	if ((ext = get_file_extension(finalpath)) == NULL) {
+		log_debug("Cannot get file extension from %s", finalpath);
+	} else {
+		if ((mime = get_mime_type(ext)) == NULL)
+			log_debug("Cannot get MIME type for %s", ext);
+	}
+
+	//user_log(0, "%s", finalpath);
+	log_access(cc, finalpath);
+
+	if (send_response(cc->ssl_peer, STATUS_SUCCESS, finalpath, mime) < 0) {
+		log_warn("Sending response to client failed");
+		return NULL;
+	}
+
+	free(ext);
+	free(mime);
+
+	return NULL;
+}
+
+/*
+ * Gemini requests are a single CRLF-terminated line with the following structure:
+ *
+ * <URL><CR><LF>
+ *
+ * <URL> is a UTF-8 encoded absolute URL, including a scheme, of maximum length
+ * 1024 bytes.
+ */
+int
+receive_gemini_request(SSL *ssl_peer, char* request_buf)
+{
+	if (SSL_read(ssl_peer, request_buf, MAXREQLEN) <= 0)
+		return -1;
+
+	return 0;
+}
+
+static void
+open_sockets(int tcpsock[2], int port)
+{
+	struct sockaddr_in addr4;
+	struct sockaddr_in6 addr6;
+	struct sockaddr *addr;
+	socklen_t len;
+	int opt = 1;
+
+	memset(&addr4, 0, sizeof(addr4));
+	addr4.sin_family = AF_INET;
+	addr4.sin_port = htons(port);
+	addr4.sin_addr.s_addr = INADDR_ANY;
+	addr = (struct sockaddr*)&addr4;
+	len = sizeof(addr4);
+
+	if ((tcpsock[0] = socket(AF_INET, SOCK_STREAM, 0)) != -1) {
+		if (setsockopt(tcpsock[0], SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
+			== -1)
+			log_warn("setting SO_REUSEADDR on socket");
+		if (bind(tcpsock[0], addr, len) == -1) {
+			close(tcpsock[0]);
+			tcpsock[0] = -1;
+		}
+		if (listen(tcpsock[0], 5) == -1) {
+			close(tcpsock[0]);
+			tcpsock[0] = -1;
+		}
+	}
+
+	memset(&addr6, 0, sizeof(addr6));
+	addr6.sin6_family = AF_INET6;
+	addr6.sin6_port = htons(port);
+	addr6.sin6_addr = in6addr_any;
+	addr = (struct sockaddr*)&addr6;
+	len = sizeof(addr6);
+
+	if ((tcpsock[1] = socket(AF_INET6, SOCK_STREAM, 0)) != -1) {
+		if (setsockopt(tcpsock[1], SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))
+			== -1)
+			log_warn("setting SO_REUSEADDR on socket");
+		if (bind(tcpsock[1], addr, len) == -1) {
+			close(tcpsock[1]);
+			tcpsock[1] = -1;
+		}
+		if (listen(tcpsock[1], 5) == -1) {
+			close(tcpsock[1]);
+			tcpsock[1] = -1;
+		}
+	}
+
+	if (tcpsock[0] == -1 && tcpsock[1] == -1) {
+		fatalx("Cannot bind to 0.0.0.0 or :: on Port 1965");
+	}
+}
+
+int
+open_pid_file(void)
+{
+	char buf[PID_BUF_SIZE];
+	char pid_path[MAXREQLEN];
+	int fd;
+
+	snprintf(pid_path, MAXREQLEN, "%s/%s",
+		_PATH_TWIND_CHROOT, _PATH_TWIND_PID);
+	if ((fd = open(pid_path, O_CREAT|O_RDWR, 0600)) == -1)
+		fatalx("Cannot open PID file");
+
+	if (flock(fd, LOCK_EX|LOCK_NB) == -1)
+		fatalx("Cannot get lock on PID file. Another instance running?");
+
+	/*
+	 * We need to truncate the file since the new PID could be shorter than
+	 * an old one in the file.
+	 */
+	if (ftruncate(fd, 0) == -1)
+		fatalx("Cannot truncate PID file");
+
+	snprintf(buf, PID_BUF_SIZE, "%ld\n", (long) getpid());
+	if (write(fd, buf, strlen(buf)) != (ssize_t)strlen(buf))
+		fatalx("Cannot write PID file");
+
+	return fd;
+}
+
+static void
+drop_root(void)
+{
+	struct passwd *pw;
+
+	if (!(pw = getpwnam(TWIND_USER)))
+		fatalx("Cannot find user entry for %s", TWIND_USER);
+
+	if (!pw->pw_uid)
+		fatalx("Cannot get UID entry for %s", TWIND_USER);
+
+#ifdef __OpenBSD__
+	if (unveil(_PATH_TWIND_CERT, "r") == -1)
+		fatalx("unveil");
+	if (unveil(_PATH_TWIND_KEY, "r") == -1)
+		fatalx("unveil");
+	if (unveil(_PATH_TWIND_CHROOT, "r") == -1)
+		fatalx("unveil");
+	if (unveil(_PATH_TWIND_PID_CHROOT, "r") == -1)
+		fatalx("unveil");
+	if (unveil(_PATH_TWIND_LOGS, "cw") == -1)
+		log_warn("unveil");
+	if (unveil(NULL, NULL) == -1)
+		fatalx("unveil");
+#endif /* __OpenBSD__ */
+
+	if (chroot(_PATH_TWIND_CHROOT) == -1)
+		fatalx("chroot() to %s failed", _PATH_TWIND_CHROOT);
+	if (chdir("/") == -1)
+		fatalx("chdir() failed");
+
+	if (setgroups(1, &pw->pw_gid) == -1)
+		fatalx("Cannot set group access list");
+	if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) == -1)
+		fatalx("Cannot set GUID to %d", pw->pw_gid);
+	if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) == -1)
+		fatalx("Cannot set UID to %d", pw->pw_uid);
+
+}
+
blob - /dev/null
blob + 9d9fce3ad479da1a3dd3a06969b71160dba3500b (mode 644)
--- /dev/null
+++ twind.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2021 Matthias Schmidt <xhr@giessen.ccc.de>
+ *
+ * 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.
+ */
+
+#ifndef _TWIND_H
+#define _TWIND_H
+
+#include <netinet/in.h>
+
+#include <openssl/ssl.h>
+
+#define VERSION "2021.a"
+#define MAXREQLEN 1025
+#define _PATH_TWIND_ACCESS_LOG "logs/access.log"
+#define _PATH_TWIND_ERROR_LOG "logs/error.log"
+
+enum status_codes {
+	STATUS_INPUT = 10,
+	STATUS_SENSITIVE_INPUT = 11,
+	STATUS_SUCCESS = 20,
+	STATUS_REDIRECT_TEMP = 30,
+	STATUS_REDIRECT_PERM = 31,
+	STATUS_TEMP_UNAVAILABLE = 40,
+	STATUS_SERVER_UNAVAILABLE = 41,
+	STATUS_CGI_ERROR = 42,
+	STATUS_PROXY_ERROR = 43,
+	STATUS_SLOW_DOWN = 44,
+	STATUS_PERM_FAILURE = 50,
+	STATUS_NOT_FOUND = 51,
+	STATUS_GONE = 52,
+	STATUS_PROXY_REQUEST_REFUSED = 53,
+	STATUS_BAD_REQUEST = 59,
+	STATUS_CLIENT_CERT_REQUIRED = 60,
+	STATUS_CERT_NOT_AUTHORIZED = 61,
+	STATUS_CERT_NOT_VALID = 62,
+};
+
+struct client_connection {
+	SSL *ssl_peer;
+	char client_addr[INET6_ADDRSTRLEN];
+};
+
+/* gemini.c */
+int check_gemini_file(const char *);
+int send_response(SSL*, int, const char *, const char *);
+int send_non_success_response(SSL*, int);
+
+/* request.c */
+int get_path_from_request(char *, char *);
+
+/* mime.c */
+char* get_file_extension(const char*);
+char* get_mime_type(const char *);
+
+/* util.c */
+void* xmalloc(size_t);
+char* xstrdup(const char *);
+size_t strlcpy(char *, const char *, size_t);
+
+/* log.c */
+void open_twind_logs(void);
+void close_twind_logs(void);
+void log_access(const struct client_connection *, const char *, ...);
+void log_error(const struct client_connection *, const char *, ...);
+void user_log(int, const char *, ...);
+
+#endif
blob - /dev/null
blob + 1f836d99eb7213ca9e0ddfbcca07ca1d31db3817 (mode 644)
--- /dev/null
+++ util.c
@@ -0,0 +1,96 @@
+/* Author: Tatu Ylonen <ylo@cs.hut.fi>
+ * Copyright (c) 1995 Tatu Ylonen <ylo@cs.hut.fi>, Espoo, Finland
+ *                    All rights reserved
+ * Versions of malloc and friends that check their results, and never return
+ * failure (they call fatal if they encounter an error).
+ *
+ * As far as I am concerned, the code I have written for this software
+ * can be used freely for any purpose.  Any derived versions of this
+ * software must be clearly marked as such, and if the derived work is
+ * incompatible with the protocol description in the RFC file, it must be
+ * called by a name other than "ssh" or "Secure Shell".
+ */
+
+#include <sys/types.h>
+
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "log.h"
+#include "twind.h"
+
+void *
+xmalloc(size_t size)
+{
+	void *ptr;
+
+	if (size == 0)
+		fatal("xmalloc: zero size");
+	ptr = malloc(size);
+	if (ptr == NULL)
+		fatal("xmalloc: out of memory (allocating %zu bytes)", size);
+	return ptr;
+}
+
+char *
+xstrdup(const char *str)
+{
+	size_t len;
+	char *cp;
+
+	len = strlen(str) + 1;
+	cp = xmalloc(len);
+	strlcpy(cp, str, len);
+	return cp;
+}
+
+/*
+ * Copyright (c) 1998, 2015 Todd C. Miller <millert@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.
+ */
+
+
+#if !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) &&\
+	!defined(__DragonFly__)
+/*
+ * Copy string src to buffer dst of size dsize.  At most dsize-1
+ * chars will be copied.  Always NUL terminates (unless dsize == 0).
+ * Returns strlen(src); if retval >= dsize, truncation occurred.
+ */
+size_t
+strlcpy(char *dst, const char *src, size_t dsize)
+{
+	const char *osrc = src;
+	size_t nleft = dsize;
+
+	/* Copy as many bytes as will fit. */
+	if (nleft != 0) {
+		while (--nleft != 0) {
+			if ((*dst++ = *src++) == '\0')
+				break;
+		}
+	}
+
+	/* Not enough room in dst, add NUL and traverse rest of src. */
+	if (nleft == 0) {
+		if (dsize != 0)
+			*dst = '\0';		/* NUL-terminate dst */
+		while (*src++)
+			;
+	}
+
+	return(src - osrc - 1);	/* count does not include NUL */
+}
+#endif /* __BSD__ */