summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorthe xhr <xhr@giessen.ccc.de>2021-08-24 21:22:29 +0200
committerthe xhr <xhr@giessen.ccc.de>2021-08-24 21:22:29 +0200
commitf224a3fe688bf2ae4c108d6b4359df4d8f03db99 (patch)
tree1d9d6fdce5671be7ae5754f51c6ef5b47bf7f0c1
parent0f9f2d04f9c340c761a9c900fac1c225b5a05419 (diff)
Initial commit of twind - a small and simple gemini daemon
-rw-r--r--.gitignore2
-rw-r--r--Makefile49
-rw-r--r--README64
-rw-r--r--gemini.c191
-rw-r--r--log.c287
-rw-r--r--log.h46
-rw-r--r--mime.c85
-rw-r--r--regress/Makefile13
-rw-r--r--regress/index.gmi22
-rwxr-xr-xregress/run_tests.sh134
-rwxr-xr-xregress/run_tests_fast.sh87
-rw-r--r--regress/twind.cert.pem17
-rw-r--r--regress/twind.csr.pem15
-rw-r--r--regress/twind.key.pem27
-rw-r--r--request.c161
-rw-r--r--twind.885
-rw-r--r--twind.c544
-rw-r--r--twind.h79
-rw-r--r--util.c96
19 files changed, 2004 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4a6e155
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+*.o
+twind
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..20824e6
--- /dev/null
+++ b/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)
diff --git a/README b/README
index e69de29..423f4ba 100644
--- a/README
+++ b/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.
diff --git a/gemini.c b/gemini.c
new file mode 100644
index 0000000..ea78c82
--- /dev/null
+++ b/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;
+}
diff --git a/log.c b/log.c
new file mode 100644
index 0000000..eca6308
--- /dev/null
+++ b/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);
+}
diff --git a/log.h b/log.h
new file mode 100644
index 0000000..e6fe919
--- /dev/null
+++ b/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 */
diff --git a/mime.c b/mime.c
new file mode 100644
index 0000000..8c4522e
--- /dev/null
+++ b/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;
+}
diff --git a/regress/Makefile b/regress/Makefile
new file mode 100644
index 0000000..369c090
--- /dev/null
+++ b/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
diff --git a/regress/index.gmi b/regress/index.gmi
new file mode 100644
index 0000000..f031ec6
--- /dev/null
+++ b/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 $
diff --git a/regress/run_tests.sh b/regress/run_tests.sh
new file mode 100755
index 0000000..f67db9f
--- /dev/null
+++ b/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
+}
+
diff --git a/regress/run_tests_fast.sh b/regress/run_tests_fast.sh
new file mode 100755
index 0000000..021bad4
--- /dev/null
+++ b/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
+
+
diff --git a/regress/twind.cert.pem b/regress/twind.cert.pem
new file mode 100644
index 0000000..b36c758
--- /dev/null
+++ b/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-----
diff --git a/regress/twind.csr.pem b/regress/twind.csr.pem
new file mode 100644
index 0000000..8b73ba8
--- /dev/null
+++ b/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-----
diff --git a/regress/twind.key.pem b/regress/twind.key.pem
new file mode 100644
index 0000000..c9d09bf
--- /dev/null
+++ b/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-----
diff --git a/request.c b/request.c
new file mode 100644
index 0000000..609f368
--- /dev/null
+++ b/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;
+}
+
diff --git a/twind.8 b/twind.8
new file mode 100644
index 0000000..6e5619c
--- /dev/null
+++ b/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 .
diff --git a/twind.c b/twind.c
new file mode 100644
index 0000000..b7d10f0
--- /dev/null
+++ b/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);
+
+}
+
diff --git a/twind.h b/twind.h
new file mode 100644
index 0000000..9d9fce3
--- /dev/null
+++ b/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
diff --git a/util.c b/util.c
new file mode 100644
index 0000000..1f836d9
--- /dev/null
+++ b/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__ */
generated by cgit on OpenBSD