commit f224a3fe688bf2ae4c108d6b4359df4d8f03db99 from: the xhr date: Tue Aug 24 19:22:29 2021 UTC Initial commit of twind - a small and simple gemini daemon 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 +# + +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 + * + * 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 +#include + +#include +#include +#include +#include +#include +#include + +#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; + + // + + 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 + * + * 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 +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 + * + * 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 +#include + +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 + * + * 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 +#include + +#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 + * + * 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 +#include +#include + +#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 + * + * 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 +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if !defined(__FreeBSD__) && !defined(__OpenBSD__) && !defined(__NetBSD__) &&\ + !defined(__DragonFly__) +#include +#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: + * + * + * + * 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 + * + * 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 + +#include + +#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 + * Copyright (c) 1995 Tatu Ylonen , 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 + +#include +#include +#include + +#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 + * + * 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__ */