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