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;
        }