add initial gopher over TLS support - gopherproxy-c - Gopher HTTP proxy in C (CGI)
(HTM) git clone git://git.codemadness.org/gopherproxy-c
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) LICENSE
---
(DIR) commit b52a2076670c215f88202f0062cbe101b4954055
(DIR) parent f8d0a722a5cb43ef0d208b11dd377ea3b02a8695
(HTM) Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date: Sun, 3 Aug 2025 21:59:39 +0200
add initial gopher over TLS support
Diffstat:
M Makefile | 15 ++++++++++++---
M README | 18 ++++++++++++++++++
M gopherproxy.c | 411 ++++++++++++++++++++++++++-----
3 files changed, 375 insertions(+), 69 deletions(-)
---
(DIR) diff --git a/Makefile b/Makefile
@@ -7,12 +7,21 @@ PREFIX = /usr/local
BINDIR = ${PREFIX}/bin
MANDIR = ${PREFIX}/man/man1
+# no TLS: fallback gophers:// to gopher://
+#GOPHER_CFLAGS = ${CFLAGS}
+#GOPHER_LDFLAGS = ${LDFLAGS}
+#GOPHER_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE
+
+# build static without TLS: useful in www chroot.
+#GOPHER_LDFLAGS = ${LDFLAGS} -static
+
+# TLS
GOPHER_CFLAGS = ${CFLAGS}
-GOPHER_LDFLAGS = ${LDFLAGS}
-GOPHER_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE
+GOPHER_LDFLAGS = -ltls ${LDFLAGS}
+GOPHER_CPPFLAGS = -D_DEFAULT_SOURCE -D_GNU_SOURCE -D_BSD_SOURCE -DUSE_TLS
# build static: useful in www chroot.
-GOPHER_LDFLAGS = ${LDFLAGS} -static
+GOPHER_LDFLAGS = -ltls -lssl -lcrypto ${LDFLAGS} -static
SRC = gopherproxy.c
OBJ = ${SRC:.c=.o}
(DIR) diff --git a/README b/README
@@ -8,6 +8,7 @@ Build dependencies
- libc + some BSD extensions (dprintf).
- POSIX system.
- make (optional).
+- LibreSSL libtls for gophers:// support (optional).
Features
@@ -16,6 +17,7 @@ Features
- Works in older browsers such as links, lynx, w3m, dillo, etc.
- No Javascript or CSS required.
- Gopher+ is not supported.
+- Support for Gopher over TLS encryption (gophers://).
Cons
@@ -24,6 +26,22 @@ Cons
- Not all gopher types are supported.
+Gopher over TLS
+---------------
+
+For a description of the protocol see near the section "TLS support":
+gopher://bitreich.org/1/scm/gopher-protocol/file/gopher-extension.md.gph
+
+For a server implementation see:
+- geomyidae: gopher://bitreich.org/1/scm/geomyidae
+
+For client implementations that support it see:
+
+- cURL: https://curl.se/
+- sacc: gopher://bitreich.org/1/scm/sacc
+- hurl: gopher://codemadness.org/1/git/hurl
+
+
CGI configuration examples
--------------------------
(DIR) diff --git a/gopherproxy.c b/gopherproxy.c
@@ -5,17 +5,33 @@
#include <ctype.h>
#include <errno.h>
#include <netdb.h>
+#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
+#include <tls.h>
+
+#ifndef TLS_CA_CERT_FILE
+#define TLS_CA_CERT_FILE "/etc/ssl/cert.pem"
+#endif
+
+#ifdef USE_TLS
+static int usetls = 0;
+/* TLS context */
+static struct tls *t;
+/* TLS config */
+static struct tls_config *tls_config;
+#endif
+
#define MAX_RESPONSETIMEOUT 10 /* timeout in seconds */
#define MAX_RESPONSESIZ 4000000 /* max download size in bytes */
#ifndef __OpenBSD__
-#define pledge(a,b) 0
+#define pledge(p1,p2) 0
+#define unveil(p1,p2) 0
#endif
/* URI */
@@ -37,7 +53,59 @@ struct visited {
char port[8];
};
+/* parsed URI */
+static struct uri u;
+/* socket fd */
+static int sock = -1;
+
int headerset = 0, isdir = 0;
+ssize_t (*readbuf)(char *, size_t);
+ssize_t (*writebuf)(const char *, size_t);
+
+void
+sighandler(int signo)
+{
+ if (signo == SIGALRM)
+ _exit(2);
+}
+
+/* print to stderr, print error message of errno and exit().
+ * Unlike BSD err() it does not prefix __progname */
+void
+err(int exitstatus, const char *fmt, ...)
+{
+ va_list ap;
+ int saved_errno;
+
+ saved_errno = errno;
+
+ if (fmt) {
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputs(": ", stderr);
+ }
+ fprintf(stderr, "%s\n", strerror(saved_errno));
+
+ exit(exitstatus);
+}
+
+/* print to stderr and exit().
+ * Unlike BSD errx() it does not prefix __progname */
+void
+errx(int exitstatus, const char *fmt, ...)
+{
+ va_list ap;
+
+ if (fmt) {
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ }
+ fputs("\n", stderr);
+
+ exit(exitstatus);
+}
void
die(int code, const char *fmt, ...)
@@ -121,14 +189,13 @@ edial(const char *host, const char *port)
struct addrinfo hints, *res, *res0;
int error, save_errno, s;
const char *cause = NULL;
- struct timeval timeout;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICSERV; /* numeric port only */
if ((error = getaddrinfo(host, port, &hints, &res0)))
- die(500, "%s: %s: %s:%s\n", __func__, gai_strerror(error), host, port);
+ die(500, "%s: %s: %s:%s", __func__, gai_strerror(error), host, port);
s = -1;
for (res = res0; res; res = res->ai_next) {
s = socket(res->ai_family, res->ai_socktype,
@@ -138,16 +205,6 @@ edial(const char *host, const char *port)
continue;
}
- timeout.tv_sec = MAX_RESPONSETIMEOUT;
- timeout.tv_usec = 0;
- if (setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == -1)
- die(500, "%s: setsockopt: %s\n", __func__, strerror(errno));
-
- timeout.tv_sec = MAX_RESPONSETIMEOUT;
- timeout.tv_usec = 0;
- if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == -1)
- die(500, "%s: setsockopt: %s\n", __func__, strerror(errno));
-
if (connect(s, res->ai_addr, res->ai_addrlen) == -1) {
cause = "connect";
save_errno = errno;
@@ -159,12 +216,199 @@ edial(const char *host, const char *port)
break;
}
if (s == -1)
- die(500, "%s: %s: %s:%s\n", __func__, cause, host, port);
+ die(500, "%s: %s: %s:%s", __func__, cause, host, port);
freeaddrinfo(res0);
return s;
}
+void
+setup_plain(void)
+{
+ if (pledge("stdio dns inet", NULL) == -1)
+ err(1, "pledge");
+
+ sock = edial(u.host, u.port);
+}
+
+#ifdef USE_TLS
+void
+setup_tls(void)
+{
+ if (tls_init())
+ errx(1, "tls_init failed");
+ if (!(tls_config = tls_config_new()))
+ errx(1, "tls config failed");
+ if (unveil(TLS_CA_CERT_FILE, "r") == -1)
+ err(1, "unveil: %s", TLS_CA_CERT_FILE);
+#if 0
+ if (tls_config_set_ca_file(tls_config, TLS_CA_CERT_FILE) == -1)
+ errx(1, "tls_config_set_ca_file: %s: %s", TLS_CA_CERT_FILE,
+ tls_config_error(tls_config));
+#endif
+
+ if (pledge("stdio dns inet rpath", NULL) == -1)
+ err(1, "pledge");
+
+ if (!(t = tls_client()))
+ errx(1, "tls_client: %s", tls_error(t));
+ if (tls_configure(t, tls_config))
+ errx(1, "tls_configure: %s", tls_error(t));
+
+ sock = edial(u.host, u.port);
+ if (tls_connect_socket(t, sock, u.host) == -1)
+ die(500, "tls_connect: %s", tls_error(t));
+}
+
+ssize_t
+tls_writebuf(const char *buf, size_t buflen)
+{
+ const char *errstr;
+ const char *p;
+ size_t len;
+ ssize_t r, written = 0;
+
+ for (len = buflen, p = buf; len > 0; ) {
+ r = tls_write(t, p, len);
+ if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) {
+ continue;
+ } else if (r == -1) {
+ errstr = tls_error(t);
+ fprintf(stderr, "tls_write: %s\n", errstr ? errstr : "");
+ return -1;
+ }
+ p += r;
+ len -= r;
+ written += r;
+ }
+ return written;
+}
+
+ssize_t
+tls_readbuf(char *buf, size_t bufsiz)
+{
+ const char *errstr;
+ ssize_t r, len;
+
+ for (len = 0; bufsiz > 0;) {
+ r = tls_read(t, buf + len, bufsiz);
+ if (r == TLS_WANT_POLLIN || r == TLS_WANT_POLLOUT) {
+ continue;
+ } else if (r == 0) {
+ break;
+ } else if (r == -1) {
+ errstr = tls_error(t);
+ fprintf(stderr, "tls_read: %s\n", errstr ? errstr : "");
+ return -1;
+ }
+ len += r;
+ bufsiz -= r;
+ }
+ return len;
+}
+#endif
+
+ssize_t
+plain_writebuf(const char *buf, size_t buflen)
+{
+ ssize_t r;
+
+ if ((r = write(sock, buf, buflen)) == -1)
+ fprintf(stderr, "write: %s\n", strerror(errno));
+
+ return r;
+}
+
+ssize_t
+plain_readbuf(char *buf, size_t bufsiz)
+{
+ ssize_t r, len;
+
+ for (len = 0; bufsiz > 0;) {
+ r = read(sock, buf + len, bufsiz);
+ if (r == 0) {
+ break;
+ } else if (r == -1) {
+ fprintf(stderr, "read: %s\n", strerror(errno));
+ return -1;
+ }
+ len += r;
+ bufsiz -= r;
+ }
+ return len;
+}
+
+struct linebuf {
+ /* line buffer */
+ char line[2048];
+ size_t linelen;
+ size_t lineoff;
+ /* read buffer */
+ char buf[4096];
+ char *bufoff, *bufend;
+ int err;
+};
+
+void
+linebuf_init(struct linebuf *b)
+{
+ memset(b, 0, sizeof(struct linebuf));
+}
+
+ssize_t
+linebuf_get(struct linebuf *b)
+{
+ size_t len;
+ ssize_t n;
+ char *p;
+
+ while (!(b->err)) {
+ /* need to read more */
+ if (b->bufoff >= b->bufend) {
+ b->bufoff = b->buf;
+ b->bufend = b->buf;
+ n = readbuf(b->buf, sizeof(b->buf));
+ if (n == -1)
+ b->err = EIO;
+
+ /* use remaining data even if not terminated by a newline */
+ if (n == 0 && b->linelen > 0)
+ return b->linelen;
+
+ if (n > 0)
+ b->bufend = b->buf + n;
+ else
+ return n;
+ }
+
+ /* search first newline */
+ if ((p = memchr(b->bufoff, '\n', b->bufend - b->bufoff))) {
+ len = (p - b->bufoff);
+ } else {
+ /* copy remaining data into line buffer and read more */
+ len = (b->bufend - b->bufoff);
+ }
+
+ if (b->lineoff + len + 1 >= sizeof(b->line)) {
+ b->err = ENOMEM;
+ return -1;
+ }
+ memcpy(b->line + b->lineoff, b->bufoff, len);
+ b->lineoff += len;
+ b->linelen = b->lineoff;
+ b->line[b->linelen] = '\0';
+
+ if (p) {
+ b->bufoff = p + 1; /* after newline */
+ b->lineoff = 0; /* reset line: start at beginning */
+ return b->linelen;
+ } else {
+ b->bufoff = b->bufend; /* read more */
+ }
+ }
+ return -1; /* UNREACHED */
+}
+
int
isblacklisted(const char *host, const char *port, const char *path)
{
@@ -210,18 +454,16 @@ void
servefile(const char *server, const char *port, const char *path, const char *query)
{
char buf[1024];
- int r, w, fd;
+ int r, w;
size_t totalsiz = 0;
- fd = edial(server, port);
+ w = snprintf(buf, sizeof(buf), "%s%s%s\r\n", path, query[0] ? "?" : "", query);
+ if (w < 0 || (size_t)w >= sizeof(buf))
+ die(500, "servefile: path too long\n");
+ if (writebuf(buf, w) == -1)
+ die(500, "servefile: writebuf failed\n");
- if (pledge("stdio", NULL) == -1)
- die(500, "pledge: %s\n", strerror(errno));
-
- if ((w = dprintf(fd, "%s%s%s\r\n", path, query[0] ? "?" : "", query)) == -1)
- die(500, "dprintf: %s\n", strerror(errno));
-
- while ((r = read(fd, buf, sizeof(buf))) > 0) {
+ while ((r = readbuf(buf, sizeof(buf))) > 0) {
/* too big total response */
totalsiz += r;
if (totalsiz > MAX_RESPONSESIZ) {
@@ -234,55 +476,57 @@ servefile(const char *server, const char *port, const char *path, const char *qu
}
if (r == -1)
die(500, "read: %s\n", strerror(errno));
- close(fd);
}
void
servedir(const char *server, const char *port, const char *path, const char *query, const char *param)
{
struct visited v;
- FILE *fp;
- char line[1024], uri[1024], primarytype;
+ struct linebuf lb;
+ const char *prefix = "";
+ char buf[1024], uri[2048];
+ char *line;
size_t totalsiz, linenr;
ssize_t n;
- int fd, r, i, len;
+ char primarytype = '\0';
+ int i, len, w;
- fd = edial(server, port);
-
- if (pledge("stdio", NULL) == -1)
- die(500, "pledge: %s\n", strerror(errno));
+#ifdef USE_TLS
+ if (usetls)
+ prefix = "gophers://";
+#endif
if (param[0])
- r = dprintf(fd, "%s%s%s\t%s\r\n", path, query[0] ? "?" : "", query, param);
+ w = snprintf(buf, sizeof(buf), "%s%s%s\t%s\r\n", path, query[0] ? "?" : "", query, param);
else
- r = dprintf(fd, "%s%s%s\r\n", path, query[0] ? "?" : "", query);
- if (r == -1)
- die(500, "write: %s\n", strerror(errno));
+ w = snprintf(buf, sizeof(buf), "%s%s%s\r\n", path, query[0] ? "?" : "", query);
- if (!(fp = fdopen(fd, "rb+")))
- die(500, "fdopen: %s\n", strerror(errno));
+ if (w < 0 || (size_t)w >= sizeof(buf))
+ die(500, "servedir: path too long\n");
+ if (writebuf(buf, w) == -1)
+ die(500, "servedir: writebuf failed\n");
+
+ linebuf_init(&lb);
+ line = lb.line;
totalsiz = 0;
- primarytype = '\0';
- for (linenr = 1; fgets(line, sizeof(line), fp); linenr++) {
- n = strcspn(line, "\n");
- if (line[n] != '\n')
- die(500, "%s:%s %s:%d: line too long\n",
- server, port, path, linenr);
- if (n && line[n] == '\n')
- line[n] = '\0';
- if (n && line[n - 1] == '\r')
- line[--n] = '\0';
- if (n == 1 && line[0] == '.')
- break;
+ for (linenr = 1; (n = linebuf_get(&lb)) > 0; linenr++) {
/* too big total response */
- totalsiz += n;
+ if (n > 0)
+ totalsiz += n;
if (totalsiz > MAX_RESPONSESIZ) {
dprintf(1, "--- transfer too big, truncated ---\n");
break;
}
+ if (n > 0 && line[n - 1] == '\n')
+ line[--n] = '\0';
+ if (n > 0 && line[n - 1] == '\r')
+ line[--n] = '\0';
+ if (n == 1 && line[0] == '.')
+ break;
+
memset(&v, 0, sizeof(v));
v._type = line[0];
@@ -349,11 +593,11 @@ servedir(const char *server, const char *port, const char *path, const char *que
}
if (!strcmp(v.port, "70"))
- snprintf(uri, sizeof(uri), "%s/%c%s",
- v.server, primarytype, v.path);
+ snprintf(uri, sizeof(uri), "%s%s/%c%s",
+ prefix, v.server, primarytype, v.path);
else
- snprintf(uri, sizeof(uri), "%s:%s/%c%s",
- v.server, v.port, primarytype, v.path);
+ snprintf(uri, sizeof(uri), "%s%s:%s/%c%s",
+ prefix, v.server, v.port, primarytype, v.path);
switch (primarytype) {
case 'i': /* info */
@@ -388,7 +632,7 @@ servedir(const char *server, const char *port, const char *path, const char *que
xmlencode(v.username);
fputs("</a>", stdout);
break;
- case 'I': /* image: show inline */
+ case 'I': /* image: show inline */
fputs(typestr(v._type), stdout);
fputs(" <a href=\"?q=", stdout);
encodeparam(uri);
@@ -416,9 +660,8 @@ servedir(const char *server, const char *port, const char *path, const char *que
}
putchar('\n');
}
- if (ferror(fp))
- die(500, "fgets: %s\n", strerror(errno));
- fclose(fp);
+ if (lb.err)
+ die(500, "%s:%s after line %d: error reading line\n", server, port, linenr);
}
int
@@ -621,14 +864,18 @@ parsepath:
int
main(void)
{
- struct uri u;
const char *p, *qs, *path, *showuri = "";
char query[1024] = "", param[1024] = "", fulluri[4096];
int r, _type = '1';
- if (pledge("stdio inet dns", NULL) == -1)
+ if (pledge("stdio inet dns rpath unveil", NULL) == -1)
die(500, "pledge: %s\n", strerror(errno));
+#ifdef MAX_RESPONSETIMEOUT
+ signal(SIGALRM, sighandler);
+ alarm(MAX_RESPONSETIMEOUT);
+#endif
+
if (!(qs = getenv("QUERY_STRING")))
qs = "";
if ((p = getparam(qs, "q"))) {
@@ -648,8 +895,12 @@ main(void)
showuri = query + sizeof("gopher://") - 1;
r = snprintf(fulluri, sizeof(fulluri), "%s", query);
} else if (!strncmp(query, "gophers://", sizeof("gophers://") - 1)) {
- showuri = query + sizeof("gophers://") - 1;
+ /* if "gophers://" is used then keep it so TLS is kept being used */
+ showuri = query;
r = snprintf(fulluri, sizeof(fulluri), "%s", query);
+#ifdef USE_TLS
+ usetls = 1;
+#endif
} else {
showuri = query;
if (uri_hasscheme(query))
@@ -687,6 +938,23 @@ main(void)
if (isblacklisted(u.host, u.port, path))
die(403, "%s:%s %s: blacklisted\n", u.host, u.port, path);
+#ifdef USE_TLS
+ /* setup TLS or plain connection */
+ if (usetls) {
+ setup_tls();
+ readbuf = tls_readbuf;
+ writebuf = tls_writebuf;
+ } else
+#endif
+ {
+ setup_plain();
+ readbuf = plain_readbuf;
+ writebuf = plain_writebuf;
+ }
+
+ if (pledge("stdio", NULL) == -1)
+ err(1, "pledge");
+
headerset = 1;
switch (_type) {
case '1':
@@ -695,11 +963,11 @@ main(void)
case '0':
dprintf(1, "Content-Type: text/plain; charset=utf-8\r\n\r\n");
servefile(u.host, u.port, path, u.query);
- return 0;
+ goto cleanup;
case 'g':
dprintf(1, "Content-Type: image/gif\r\n\r\n");
servefile(u.host, u.port, path, u.query);
- return 0;
+ goto cleanup;
case 'I':
/* try to set Content-Type based on extension */
if ((p = strrchr(path, '.'))) {
@@ -713,18 +981,18 @@ main(void)
}
write(1, "\r\n", 2);
servefile(u.host, u.port, path, u.query);
- return 0;
+ goto cleanup;
case '9':
/* try to detect filename */
if ((p = strrchr(path, '/')))
dprintf(1, "Content-Disposition: attachment; filename=\"%s\"\r\n", p + 1);
dprintf(1, "Content-Type: application/octet-stream\r\n\r\n");
servefile(u.host, u.port, path, u.query);
- return 0;
+ goto cleanup;
default:
write(1, "\r\n", 2);
servefile(u.host, u.port, path, u.query);
- return 0;
+ goto cleanup;
}
}
@@ -767,5 +1035,16 @@ main(void)
fputs("</pre>\n</body>\n</html>\n", stdout);
+cleanup:
+#ifdef USE_TLS
+ /* cleanup TLS and plain connection */
+ if (t) {
+ tls_close(t);
+ tls_free(t);
+ }
+#endif
+ if (sock != -1)
+ close(sock);
+
return 0;
}