initial repo - gopherproxy-c - Gopher HTTP proxy in C (CGI) | |
git clone git://git.codemadness.org/gopherproxy-c | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
commit 40a6ccd6cfb99c2849dff4501a54bc7752b63620 | |
Author: Hiltjo Posthuma <[email protected]> | |
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(-) | |
--- | |
diff --git a/.gitignore b/.gitignore | |
@@ -0,0 +1,2 @@ | |
+gopherproxy | |
+*.o | |
diff --git a/LICENSE b/LICENSE | |
@@ -0,0 +1,15 @@ | |
+ISC License | |
+ | |
+Copyright (c) 2018 Hiltjo Posthuma <[email protected]> | |
+ | |
+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. | |
diff --git a/Makefile b/Makefile | |
@@ -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) | |
diff --git a/README b/README | |
@@ -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. | |
diff --git a/gopherproxy.c b/gopherproxy.c | |
@@ -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", stdou… | |
+ } | |
+ | |
+ 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,… | |
+ 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(ti… | |
+ die(500, "%s: setsockopt: %s\n", __func__, strerror(er… | |
+ 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; | |
+ 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 *p… | |
+{ | |
+ 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 + size… | |
+ else | |
+ snprintf(uri, sizeof(uri), "gopher://%s:%s/%c%… | |
+ 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>", … | |
+ fputs(typestr(v._type), stdout); | |
+ fputs(" <input type=\"hidden\" name=\"q\" value=\"", s… | |
+ xmlencode(uri); | |
+ fputs("\" /><input type=\"search\" placeholder=\"", st… | |
+ xmlencode(v.username); | |
+ fputs( | |
+ "\" name=\"p\" value=\"\" size=\"72\" />" | |
+ "<input type=\"submit\" value=\"Search\" /></p… | |
+ } 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, pa… | |
+ | |
+ headerset = 1; | |
+ switch (_type) { | |
+ case '0': | |
+ printf("Content-Type: text/plain; charset=utf-8\r\n\r\… | |
+ 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; filen… | |
+ 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; charse… | |
+ "<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\"… | |
+ "<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; | |
+} |