Introduction
Introduction Statistics Contact Development Disclaimer Help
add initial gopher over TLS support - gopherproxy-c - Gopher HTTP proxy in C (C…
git clone git://git.codemadness.org/gopherproxy-c
Log
Files
Refs
README
LICENSE
---
commit b52a2076670c215f88202f0062cbe101b4954055
parent f8d0a722a5cb43ef0d208b11dd377ea3b02a8695
Author: Hiltjo Posthuma <[email protected]>
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(-)
---
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}
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
--------------------------
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), hos…
+ die(500, "%s: %s: %s:%s", __func__, gai_strerror(error), host,…
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(ti…
- die(500, "%s: setsockopt: %s\n", __func__, strerror(er…
-
- timeout.tv_sec = MAX_RESPONSETIMEOUT;
- timeout.tv_usec = 0;
- if (setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(ti…
- die(500, "%s: setsockopt: %s\n", __func__, strerror(er…
-
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 newl…
+ 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 *…
{
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] ? "?" : ""…
+ 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)) …
- 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 cha…
}
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 *q…
{
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] ? "?" : "", q…
+ w = snprintf(buf, sizeof(buf), "%s%s%s\t%s\r\n", path, query[0…
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] ? …
- 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…
}
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 *…
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 *…
}
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, …
}
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://") …
- showuri = query + sizeof("gophers://") - 1;
+ /* if "gophers://" is used then keep it so TLS is kept…
+ 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, pa…
+#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\…
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; f…
dprintf(1, "Content-Type: application/octet-stream\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;
}
You are viewing proxied material from codemadness.org. The copyright of proxied material belongs to its original authors. Any comments or complaints in relation to proxied material should be directed to the original authors of the content concerned. Please see the disclaimer for more details.