t* gopherproxy-c customized
URI git clone git://git.codevoid.de/gopherproxy-c-sdk
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
DIR commit 40a6ccd6cfb99c2849dff4501a54bc7752b63620
URI Author: Hiltjo Posthuma <hiltjo@codemadness.org>
Date: Sun, 12 Aug 2018 18:14:09 +0200
initial repo
Diffstat:
A .gitignore | 2 ++
A LICENSE | 15 +++++++++++++++
A Makefile | 17 +++++++++++++++++
A README | 17 +++++++++++++++++
A gopherproxy.c | 586 ++++++++++++++++++++++++++++++
5 files changed, 637 insertions(+), 0 deletions(-)
---
DIR diff --git a/.gitignore b/.gitignore
t@@ -0,0 +1,2 @@
+gopherproxy
+*.o
DIR diff --git a/LICENSE b/LICENSE
t@@ -0,0 +1,15 @@
+ISC License
+
+Copyright (c) 2018 Hiltjo Posthuma <hiltjo@codemadness.org>
+
+Permission to use, copy, modify, and/or 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.
DIR diff --git a/Makefile b/Makefile
t@@ -0,0 +1,17 @@
+.POSIX:
+
+BIN = gopherproxy
+OBJ = $(BIN:=.o)
+
+# OpenBSD: use pledge(2).
+#CFLAGS += -DUSE_PLEDGE
+# build static: useful in www chroot.
+#LDFLAGS += -static
+
+all: $(BIN)
+
+$(BIN): $(OBJ)
+ $(CC) $(OBJ) $(LDFLAGS) -o $@
+
+clean:
+ rm -f $(BIN) $(OBJ)
DIR diff --git a/README b/README
t@@ -0,0 +1,17 @@
+gopherproxy
+===========
+
+Build dependencies:
+- C compiler.
+- libc
+- POSIX system.
+- make (optional).
+
+
+Features:
+- Works in older browsers such as links, lynx, w3m, dillo, etc.
+- No Javascript or CSS required.
+
+
+Cons:
+- Not all gopher types are supported.
DIR diff --git a/gopherproxy.c b/gopherproxy.c
t@@ -0,0 +1,586 @@
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <netdb.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#define MAX_RESPONSETIMEOUT 10 /* timeout in seconds */
+#define MAX_RESPONSESIZ 4000000 /* max download size in bytes */
+
+#ifndef USE_PLEDGE
+#define pledge(a,b) 0
+#endif
+
+struct uri {
+ char proto[16];
+ char host[256];
+ char port[8];
+ char path[1024];
+};
+
+struct visited {
+ int _type;
+ char username[1024];
+ char path[1024];
+ char server[256];
+ char port[8];
+};
+
+int headerset = 0;
+
+void
+die(int code, const char *fmt, ...)
+{
+ va_list ap;
+
+ if (!headerset) {
+ switch (code) {
+ case 400:
+ fputs("Status: 400 Bad Request\r\n", stdout);
+ break;
+ case 403:
+ fputs("Status: 403 Permission Denied\r\n", stdout);
+ break;
+ default:
+ fputs("Status: 500 Internal Server Error\r\n", stdout);
+ break;
+ }
+ fputs("Content-Type: text/plain; charset=utf-8\r\n\r\n", stdout);
+ }
+
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+
+ va_start(ap, fmt);
+ vfprintf(stdout, fmt, ap);
+ va_end(ap);
+
+ exit(1);
+}
+
+/* Escape characters below as HTML 2.0 / XML 1.0. */
+void
+xmlencode(const char *s)
+{
+ for (; *s; s++) {
+ switch(*s) {
+ case '<': fputs("<", stdout); break;
+ case '>': fputs(">", stdout); break;
+ case '\'': fputs("'", stdout); break;
+ case '&': fputs("&", stdout); break;
+ case '"': fputs(""", stdout); break;
+ default: putchar(*s);
+ }
+ }
+}
+
+int
+dial(const char *host, const char *port)
+{
+ struct addrinfo hints, *res, *res0;
+ int error, save_errno, s;
+ const char *cause = NULL;
+ struct timeval timeout = {
+ .tv_sec = MAX_RESPONSETIMEOUT,
+ .tv_usec = 0,
+ };
+
+ 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", __func__, gai_strerror(error), host, port);
+ s = -1;
+ for (res = res0; res; res = res->ai_next) {
+ s = socket(res->ai_family, res->ai_socktype,
+ res->ai_protocol);
+ if (s == -1) {
+ cause = "socket";
+ continue;
+ }
+
+ if (setsockopt(s, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof(timeout)) == -1)
+ die(500, "%s: setsockopt: %s\n", __func__, strerror(errno));
+ 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;
+ close(s);
+ errno = save_errno;
+ s = -1;
+ continue;
+ }
+ break;
+ }
+ if (s == -1)
+ die(500, "%s: %s: %s:%s\n", __func__, cause, host, port);
+ freeaddrinfo(res0);
+
+ return s;
+}
+
+int
+isblacklisted(const char *host, const char *port, const char *path)
+{
+ char *p;
+
+ if ((p = strstr(host, ".onion")) && strlen(p) == strlen(".onion"))
+ return 1;
+ return 0;
+}
+
+char *
+typestr(int c)
+{
+ switch (c) {
+ case '0': return " TEXT";
+ case '1': return " DIR";
+ case '7': return "SEARCH";
+ case '9': return " BIN";
+ case 'g': return " GIF";
+ case 'h': return " HTML"; /* non-standard */
+ case 's': return " SND"; /* non-standard */
+ case 'A': return " AUDIO"; /* non-standard */
+ case 'I': return " IMG";
+ default: return " ";
+ }
+}
+
+void
+servefile(const char *server, const char *port, const char *path)
+{
+ char buf[1024];
+ int r, w, fd;
+ size_t totalsiz = 0;
+
+ fd = dial(server, port);
+
+ if (pledge("stdio", NULL) == -1)
+ die(500, "pledge: %s\n", strerror(errno));
+
+ w = dprintf(fd, "%s\r\n", path);
+ if (w == -1)
+ die(500, "dprintf: %s\n", strerror(errno));
+
+ while ((r = read(fd, buf, sizeof(buf))) > 0) {
+ /* too big total response */
+ totalsiz += r;
+ if (totalsiz > MAX_RESPONSESIZ) {
+ dprintf(1, "--- transfer too big, truncated ---\n");
+ break;
+ }
+
+ if ((w = write(1, buf, r)) == -1)
+ die(500, "write: %s\n", strerror(errno));
+ }
+ 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 *param)
+{
+ struct visited v;
+ FILE *fp;
+ char line[1024], uri[1024];
+ size_t totalsiz, linenr;
+ ssize_t n;
+ int fd, r, i, len;
+
+ fd = dial(server, port);
+
+ if (pledge("stdio", NULL) == -1)
+ die(500, "pledge: %s\n", strerror(errno));
+
+ if (param[0])
+ r = dprintf(fd, "%s\t%s\r\n", path, param);
+ else
+ r = dprintf(fd, "%s\r\n", path);
+ if (r == -1)
+ die(500, "write: %s\n", strerror(errno));
+
+ if (!(fp = fdopen(fd, "rb+")))
+ die(500, "fdopen: %s\n", strerror(errno));
+
+ totalsiz = 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;
+
+ /* too big total response */
+ totalsiz += n;
+ if (totalsiz > MAX_RESPONSESIZ) {
+ dprintf(1, "--- transfer too big, truncated ---\n");
+ break;
+ }
+
+ memset(&v, 0, sizeof(v));
+
+ v._type = line[0];
+
+ /* "username" */
+ i = 1;
+ len = strcspn(line + i, "\t");
+ if (len + 1 < sizeof(v.username)) {
+ memcpy(v.username, line + i, len);
+ v.username[len] = '\0';
+ } else {
+ die(500, "%s:%s %s:%d: username field too long\n",
+ server, port, path, linenr);
+ }
+ if (line[i + len] == '\t')
+ i += len + 1;
+ else
+ die(500, "%s:%s %s:%d: invalid line / field count\n",
+ server, port, path, linenr);
+
+ /* selector / path */
+ len = strcspn(line + i, "\t");
+ if (len + 1 < sizeof(v.path)) {
+ memcpy(v.path, line + i, len);
+ v.path[len] = '\0';
+ } else {
+ die(500, "%s:%s %s:%d: path field too long\n",
+ server, port, path, linenr);
+ }
+ if (line[i + len] == '\t')
+ i += len + 1;
+ else
+ die(500, "%s:%s %s:%d: invalid line / field count\n",
+ server, port, path, linenr);
+
+ /* server */
+ len = strcspn(line + i, "\t");
+ if (len + 1 < sizeof(v.server)) {
+ memcpy(v.server, line + i, len);
+ v.server[len] = '\0';
+ } else {
+ die(500, "%s:%s %s:%d: server field too long\n",
+ server, port, path, linenr);
+ }
+ if (line[i + len] == '\t')
+ i += len + 1;
+ else
+ die(500, "%s:%s %s:%d: invalid line / field count\n",
+ server, port, path, linenr);
+
+ /* port */
+ len = strcspn(line + i, "\t");
+ if (len + 1 < sizeof(v.port)) {
+ memcpy(v.port, line + i, len);
+ v.port[len] = '\0';
+ } else {
+ die(500, "%s:%s %s:%d: port field too long\n",
+ server, port, path, linenr);
+ }
+
+ uri[0] = '\0';
+ switch (line[0]) {
+ case '7':
+ snprintf(uri, sizeof(uri), "gopher://%s:%s/%c%s",
+ v.server, v.port, v._type, v.path);
+ break;
+ case 'h':
+ if (!strncmp(v.path, "URL:", sizeof("URL:") - 1))
+ snprintf(uri, sizeof(uri), "%s", v.path + sizeof("URL:") - 1);
+ else
+ snprintf(uri, sizeof(uri), "gopher://%s:%s/%c%s",
+ v.server, v.port, v._type, v.path);
+ break;
+ case 'i': /* info */
+ case '3': /* error */
+ break;
+ default:
+ snprintf(uri, sizeof(uri), "?q=gopher://%s:%s/%c%s",
+ v.server, v.port, v._type, v.path);
+ }
+
+ /* search */
+ if (v._type == '7') {
+ fputs("</pre><form method=\"get\" action=\"\"><pre>", stdout);
+ fputs(typestr(v._type), stdout);
+ fputs(" <input type=\"hidden\" name=\"q\" value=\"", stdout);
+ xmlencode(uri);
+ fputs("\" /><input type=\"search\" placeholder=\"", stdout);
+ xmlencode(v.username);
+ fputs(
+ "\" name=\"p\" value=\"\" size=\"72\" />"
+ "<input type=\"submit\" value=\"Search\" /></pre></form><pre>", stdout);
+ } else {
+ fputs(typestr(v._type), stdout);
+ if (uri[0]) {
+ fputs(" <a href=\"", stdout);
+ xmlencode(uri);
+ fputs("\">", stdout);
+ xmlencode(v.username);
+ fputs("</a>", stdout);
+ } else {
+ fputs(" ", stdout);
+ xmlencode(v.username);
+ }
+ }
+ putchar('\n');
+ }
+ if (ferror(fp))
+ die(500, "fgets: %s\n", strerror(errno));
+ fclose(fp);
+}
+
+int
+hexdigit(int c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ else if (c >= 'A' && c <= 'F')
+ return c - 'A' + 10;
+ else if (c >= 'a' && c <= 'f')
+ return c - 'a' + 10;
+
+ return 0;
+}
+
+/* decode until NUL separator or end of "key". */
+int
+decodeparam(char *buf, size_t bufsiz, const char *s)
+{
+ size_t i;
+
+ if (!bufsiz)
+ return -1;
+
+ for (i = 0; *s && *s != '&'; s++) {
+ if (i + 3 >= bufsiz)
+ return -1;
+ switch (*s) {
+ case '%':
+ if (!isxdigit(*(s+1)) || !isxdigit(*(s+2)))
+ return -1;
+ buf[i++] = hexdigit(*(s+1)) * 16 + hexdigit(*(s+2));
+ s += 2;
+ break;
+ case '+':
+ buf[i++] = ' ';
+ break;
+ default:
+ buf[i++] = *s;
+ break;
+ }
+ }
+ buf[i] = '\0';
+
+ return i;
+}
+
+char *
+getparam(const char *query, const char *s)
+{
+ const char *p;
+ size_t len;
+
+ len = strlen(s);
+ for (p = query; (p = strstr(p, s)); p += len) {
+ if (p[len] == '=' && (p == query || p[-1] == '&'))
+ return (char *)p + len + 1;
+ }
+
+ return NULL;
+}
+
+int
+checkparam(const char *s)
+{
+ for (; *s; s++)
+ if (iscntrl(*s))
+ return 0;
+ return 1;
+}
+
+int
+parseuri(const char *str, struct uri *u)
+{
+ const char *s, *e;
+
+ memset(u, 0, sizeof(struct uri));
+
+ /* protocol part */
+ for (e = s = str; *e && (isalpha((int)*e) || isdigit((int)*e) ||
+ *e == '+' || *e == '-' || *e == '.'); e++)
+ ;
+ if (strncmp(e, "://", sizeof("://") - 1))
+ return 0;
+ if (e - s + 1 >= sizeof(u->proto))
+ return 0;
+ memcpy(u->proto, s, e - s);
+ u->proto[e - s] = '\0';
+
+ e += sizeof("://") - 1;
+ s = e;
+
+ e = &e[strcspn(s, ":/")];
+ if (e - s + 1 >= sizeof(u->host))
+ return 0;
+ memcpy(u->host, s, e - s);
+ u->host[e - s] = '\0';
+
+ if (*e == ':') {
+ s = ++e;
+
+ e = &s[strcspn(s, "/")];
+
+ if (e - s + 1 >= sizeof(u->port))
+ return 0;
+ memcpy(u->port, s, e - s);
+ u->port[e - s] = '\0';
+ }
+ if (*e && *e != '/')
+ return 0; /* invalid path */
+
+ s = e;
+ e = s + strlen(s);
+
+ if (e - s + 1 >= sizeof(u->path))
+ return 0;
+ memcpy(u->path, s, e - s);
+ u->path[e - s] = '\0';
+
+ return 1;
+}
+
+int
+main(void)
+{
+ struct uri u;
+ const char *p, *qs, *path;
+ char query[1024] = "", param[1024] = "", uri[1024] = "";
+ int _type = '1';
+
+ if (pledge("stdio inet dns", NULL) == -1)
+ die(500, "pledge: %s\n", strerror(errno));
+
+ if (!(qs = getenv("QUERY_STRING")))
+ qs = "";
+ if ((p = getparam(qs, "q"))) {
+ if (decodeparam(query, sizeof(query), p) == -1 ||
+ !checkparam(query))
+ die(400, "Invalid parameter: q\n");
+ }
+ if ((p = getparam(qs, "p"))) {
+ if (decodeparam(param, sizeof(param), p) == -1 ||
+ !checkparam(param))
+ die(400, "Invalid parameter: p\n");
+ }
+
+ path = "/";
+ if (query[0]) {
+ if (strncmp(query, "gopher://", sizeof("gopher://") - 1))
+ snprintf(uri, sizeof(uri), "gopher://%s", query);
+ else
+ snprintf(uri, sizeof(uri), "%s", query);
+
+ if (!parseuri(uri, &u))
+ die(400, "Invalid uri: %s\n", uri);
+ if (u.host[0] == '\0')
+ die(400, "Invalid hostname\n");
+
+ if (u.path[0] == '\0')
+ memcpy(u.path, "/", 2);
+ if (u.port[0] == '\0')
+ memcpy(u.port, "70", 3);
+
+ path = u.path;
+ if (path[0] == '/') {
+ if (path[1]) {
+ _type = path[1];
+ path += 2;
+ }
+ } else {
+ path = "/";
+ }
+
+ if (isblacklisted(u.host, u.port, path))
+ die(403, "%s:%s %s: blacklisted\n", u.host, u.port, path);
+
+ headerset = 1;
+ switch (_type) {
+ case '0':
+ printf("Content-Type: text/plain; charset=utf-8\r\n\r\n");
+ fflush(stdout);
+ servefile(u.host, u.port, path);
+ return 0;
+ case '1':
+ case '7':
+ break; /* handled below */
+ case '9':
+ printf("Content-Type: application/octet-stream\r\n");
+ if ((p = strrchr(path, '/')))
+ printf("Content-Disposition: attachment; filename=\"%s\"\r\n", p + 1);
+ printf("\r\n");
+ fflush(stdout);
+ servefile(u.host, u.port, path);
+ return 0;
+ default:
+ write(1, "\r\n", 2);
+ servefile(u.host, u.port, path);
+ return 0;
+ }
+ }
+
+ fputs("Content-Type: text/html; charset=utf-8\r\n\r\n", stdout);
+ headerset = 1;
+
+ fputs(
+ "<!DOCTYPE html>\n"
+ "<html dir=\"ltr\">\n"
+ "<head>\n"
+ "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n"
+ "<title>", stdout);
+ xmlencode(query);
+ if (query[0])
+ fputs(" - ", stdout);
+ fputs(
+ "Gopher HTTP proxy</title>\n"
+ "<style type=\"text/css\">a { text-decoration: none; } "
+ "a:hover { text-decoration: underline; }</style>\n"
+ "<meta name=\"robots\" content=\"noindex, nofollow\" />\n"
+ "<meta name=\"robots\" content=\"none\" />\n"
+ "<meta content=\"width=device-width\" name=\"viewport\" />\n"
+ "</head>\n"
+ "<body>\n"
+ "<form method=\"get\" action=\"\"><pre>"
+ " URI: <input type=\"search\" name=\"q\" value=\"", stdout);
+ xmlencode(uri);
+ fputs(
+ "\" placeholder=\"URI...\" size=\"72\" autofocus=\"autofocus\" class=\"search\" />"
+ "<input type=\"submit\" value=\"Go for it!\" /></pre>"
+ "</form><pre>\n", stdout);
+
+ if (query[0]) {
+ if (_type != '7')
+ param[0] = '\0';
+ servedir(u.host, u.port, path, param);
+ }
+
+ fputs("</pre>\n</body>\n</html>\n", stdout);
+
+ return 0;
+}