diff options
author | bptato <nincsnevem662@gmail.com> | 2023-09-30 18:02:48 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2023-09-30 19:02:05 +0200 |
commit | 31fbd611aa107cc023dcf1e1358f19ff58f14075 (patch) | |
tree | c3360c2f4575a249bb30f18b746ab3a0ce0c91b2 /bonus | |
parent | bb2c7cb87a99f790b6c741f85de666dd7aeae10c (diff) | |
download | chawan-31fbd611aa107cc023dcf1e1358f19ff58f14075.tar.gz |
Add urimethodmap support
yay
Diffstat (limited to 'bonus')
-rw-r--r-- | bonus/gmi2html/.gitignore | 1 | ||||
-rw-r--r-- | bonus/gmi2html/Makefile | 10 | ||||
-rw-r--r-- | bonus/gmi2html/gmi2html.c | 241 | ||||
-rw-r--r-- | bonus/gmifetch/.gitignore | 1 | ||||
-rw-r--r-- | bonus/gmifetch/Makefile | 11 | ||||
-rw-r--r-- | bonus/gmifetch/gmifetch.c | 675 | ||||
-rw-r--r-- | bonus/trans.cgi | 13 |
7 files changed, 952 insertions, 0 deletions
diff --git a/bonus/gmi2html/.gitignore b/bonus/gmi2html/.gitignore new file mode 100644 index 00000000..fd3a71e5 --- /dev/null +++ b/bonus/gmi2html/.gitignore @@ -0,0 +1 @@ +gmi2html diff --git a/bonus/gmi2html/Makefile b/bonus/gmi2html/Makefile new file mode 100644 index 00000000..f655100a --- /dev/null +++ b/bonus/gmi2html/Makefile @@ -0,0 +1,10 @@ +CFLAGS = -Wall -Wextra -std=c89 -pedantic -g -O2 -fsanitize=address +prefix = /usr/local + +gmi2html: gmi2html.c + $(CC) $(CFLAGS) gmi2html.c -o gmi2html + +.PHONY: install +install: + mkdir -p "$(DESTDIR)$(prefix)/bin" + install -m755 gmi2html "$(DESTDIR)$(prefix)/bin" diff --git a/bonus/gmi2html/gmi2html.c b/bonus/gmi2html/gmi2html.c new file mode 100644 index 00000000..869b3112 --- /dev/null +++ b/bonus/gmi2html/gmi2html.c @@ -0,0 +1,241 @@ +/* This file is dedicated to the public domain. + * + * Convert gemtext to HTML. Only accepts input on stdin. + */ + +#include <stdio.h> +#include <stdlib.h> + +typedef enum { + STATE_NORMAL, + STATE_BLOCKQUOTE, + STATE_NEWLINE, + STATE_NEWLINE_EQUALS, + STATE_NEWLINE_EQUALS_ARROW, + STATE_BEFORE_URL, + STATE_IN_URL, + STATE_BEFORE_URL_NAME, + STATE_URL_NAME, + STATE_SINGLE_BACKTICK, + STATE_DOUBLE_BACKTICK, + STATE_PRE_START, + STATE_IN_PRE, + STATE_PRE_SINGLE_BACKTICK, + STATE_PRE_DOUBLE_BACKTICK, + STATE_SKIP_LINE, + STATE_HASH, + STATE_DOUBLE_HASH, + STATE_AFTER_HASH, + STATE_AFTER_DOUBLE_HASH, + STATE_AFTER_TRIPLE_HASH +} ParseState; + +static ParseState state = STATE_NEWLINE; +static ParseState prev_state = STATE_NORMAL; + +int main() { + int c; +#define BUFSIZE 4096 + char urlbuf[BUFSIZE + 1]; + char *urlp; + + urlp = urlbuf; + printf("<!DOCTYPE html>"); +#define SET_STATE(s) do { \ + prev_state = state; \ + state = s; \ + } while (0) +#define REDO_NORMAL do { \ + SET_STATE(STATE_NORMAL); \ + goto normal; \ + } while (0) + while ((c = getc(stdin)) != EOF) { + switch (state) { + case STATE_NORMAL: + case STATE_BLOCKQUOTE: + case STATE_IN_PRE: + case STATE_PRE_START: + case STATE_SKIP_LINE: + case STATE_URL_NAME: + case STATE_AFTER_HASH: + case STATE_AFTER_DOUBLE_HASH: + case STATE_AFTER_TRIPLE_HASH: +normal: switch (c) { + case '\r': break; + case '\n': + if (state == STATE_BLOCKQUOTE) { + fputs("</blockquote>", stdout); + } else if (state == STATE_PRE_START) { + fputs("\">", stdout); + SET_STATE(STATE_IN_PRE); + } else if (state == STATE_URL_NAME) { + fputs("</a>", stdout); + fputs("<br>", stdout); + } else if (state == STATE_AFTER_HASH) { + fputs("</h1>", stdout); + } else if (state == STATE_AFTER_DOUBLE_HASH) { + fputs("</h2>", stdout); + } else if (state == STATE_AFTER_TRIPLE_HASH) { + fputs("</h3>", stdout); + } else if (state == STATE_SKIP_LINE) { + } else { + fputs("<br>", stdout); + } + SET_STATE(STATE_NEWLINE); + break; + case '<': + fputs("<", stdout); + break; + case '>': + fputs(">", stdout); + break; + case '&': + fputs("&", stdout); + break; + default: + if (state != STATE_SKIP_LINE) + putchar(c); + break; + } + break; + case STATE_NEWLINE: + if (prev_state == STATE_IN_PRE) { + if (c == '`') { + SET_STATE(STATE_PRE_SINGLE_BACKTICK); + break; + } else { + SET_STATE(STATE_IN_PRE); + goto normal; + } + } + switch (c) { + case '=': + SET_STATE(STATE_NEWLINE_EQUALS); + break; + case '>': + SET_STATE(STATE_BLOCKQUOTE); + printf("<blockquote>"); + break; + case '`': + SET_STATE(STATE_SINGLE_BACKTICK); + break; + case '#': + SET_STATE(STATE_HASH); + break; + default: + REDO_NORMAL; + } + break; + case STATE_NEWLINE_EQUALS: + if (c == '>') { + SET_STATE(STATE_NEWLINE_EQUALS_ARROW); + } else { + putchar('='); + REDO_NORMAL; + } + break; + case STATE_NEWLINE_EQUALS_ARROW: + if (c == ' ') { + state = STATE_BEFORE_URL; + } else { + putchar('='); + REDO_NORMAL; + } + break; + case STATE_BEFORE_URL: + if (c == ' ') { + continue; + break; + } else { + fputs("<a href=\"", stdout); + SET_STATE(STATE_IN_URL); + urlp = urlbuf; + } + /* fall through */ + case STATE_IN_URL: + switch (c) { + case '"': + fputs("%22", stdout); + if (urlp < &urlbuf[BUFSIZE]) + *urlp++ = '"'; + break; + case ' ': + case '\t': + fputs("\">", stdout); + *urlp = '\0'; + SET_STATE(STATE_BEFORE_URL_NAME); + break; + case '\n': + *urlp = '\0'; + fputs("\">", stdout); + fputs(urlbuf, stdout); + fputs("</a><br>", stdout); + SET_STATE(STATE_NEWLINE); + break; + default: + if (urlp < &urlbuf[BUFSIZE] && c != '>' + && c != '<') + *urlp++ = c; + putchar(c); + } + break; + case STATE_BEFORE_URL_NAME: + if (c != ' ' && c != '\t') { + SET_STATE(STATE_URL_NAME); + goto normal; + } + break; + case STATE_SINGLE_BACKTICK: + case STATE_PRE_SINGLE_BACKTICK: + if (c == '`') { + SET_STATE(state == STATE_SINGLE_BACKTICK ? + STATE_DOUBLE_BACKTICK : + STATE_PRE_DOUBLE_BACKTICK); + } else { + putchar('`'); + REDO_NORMAL; + } + break; + case STATE_DOUBLE_BACKTICK: + case STATE_PRE_DOUBLE_BACKTICK: + if (c == '`') { + if (state == STATE_DOUBLE_BACKTICK) { + SET_STATE(STATE_PRE_START); + fputs("<pre title=\"", stdout); + } else { + fputs("</pre>", stdout); + SET_STATE(STATE_SKIP_LINE); + } + } else { + fputs("``", stdout); + if (state == STATE_DOUBLE_BACKTICK) { + REDO_NORMAL; + } else { + SET_STATE(STATE_IN_PRE); + goto normal; + } + } + break; + case STATE_HASH: + if (c == '#') { + SET_STATE(STATE_DOUBLE_HASH); + } else { + fputs("<h1>", stdout); + SET_STATE(STATE_AFTER_HASH); + goto normal; + } + break; + case STATE_DOUBLE_HASH: + if (c == '#') { + fputs("<h3>", stdout); + SET_STATE(STATE_AFTER_TRIPLE_HASH); + } else { + fputs("<h2>", stdout); + SET_STATE(STATE_AFTER_DOUBLE_HASH); + goto normal; + } + break; + } + } + exit(0); +} diff --git a/bonus/gmifetch/.gitignore b/bonus/gmifetch/.gitignore new file mode 100644 index 00000000..99270cf6 --- /dev/null +++ b/bonus/gmifetch/.gitignore @@ -0,0 +1 @@ +gmifetch diff --git a/bonus/gmifetch/Makefile b/bonus/gmifetch/Makefile new file mode 100644 index 00000000..5ee37b3a --- /dev/null +++ b/bonus/gmifetch/Makefile @@ -0,0 +1,11 @@ +CFLAGS = -Wall -Wextra -std=c89 -pedantic -lcrypto -lssl -g -O2 + +gmifetch: gmifetch.c + $(CC) $(CFLAGS) gmifetch.c -o gmifetch + +.PHONY: clean +clean: + rm -f gmifetch + +.PHONY: all +all: gmifetch diff --git a/bonus/gmifetch/gmifetch.c b/bonus/gmifetch/gmifetch.c new file mode 100644 index 00000000..f10554c6 --- /dev/null +++ b/bonus/gmifetch/gmifetch.c @@ -0,0 +1,675 @@ +/* This file is dedicated to the public domain. + * + * Gemini protocol adapter for Chawan. + * Intended to be used through local CGI (by redirection in scheme-map). + * + * (FWIW, it should work with normal CGI or w3m's local CGI too. However, + * it does not rewrite URLs, so you would have to figure out something for + * that, e.g. by setting the base href or rewriting URLs in another layer.) + * + * Usage: gmifetch [URL] + * + * Environment variables: + * - QUERY_STRING is used if no URL arguments are passed. + * - GMIFETCH_KNOWN_HOSTS is used for setting the known_hosts file. If not set, + * we use $XDG_CONFIG_HOME/gmifetch/known_hosts, where $XDG_CONFIG_HOME falls + * back to $HOME/.config/gmifetch if not set. + */ + +#include <ctype.h> +#include <errno.h> +#include <openssl/err.h> +#include <openssl/pem.h> +#include <openssl/ssl.h> +#include <pwd.h> +#include <sys/stat.h> +#include <unistd.h> + +static SSL_CTX* ssl_ctx; +static SSL *ssl; +static BIO *conn; + +/* CGI responses */ +#define INPUT_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>" \ + "<title>Input required</title>" \ + "<base href='%s'>" \ + "<h1>Input required</h1>" \ + "<p>" \ + "%s" \ + "<p>" \ + "<form method=POST><input type='%s' name='input'></form>" + +#define SUCCESS_RESPONSE "Content-Type: %s\r\n" \ + "\r\n" + +#define REDIRECT_RESPONSE "Status: 30%c\r\n" \ + "Location: %s\r\n" \ + "\r\n" + +#define TEMPFAIL_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>" \ + "<title>Temporary failure</title>" \ + "<h1>%s</h1>" \ + "<p>" \ + "%s" + +#define PERMFAIL_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>" \ + "<title>Permanent failure</title>" \ + "<h1>%s</h1>" \ + "<p>" \ + "%s" + +#define CERTFAIL_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>" \ + "<title>Certificate failure</title>" \ + "<h1>%s</h1>" \ + "<p>" \ + "%s" + +#define INVALID_CERT_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>\n" \ + "<title>Invalid certificate</title>\n" \ + "<h1>Invalid certificate</h1>\n" \ + "<p>\n" \ + "The certificate received from the server does not match the\n" \ + "stored certificate (expected %s, but got %s). Somebody may be\n" \ + "tampering with your connection.\n" \ + "<p>\n" \ + "If you are sure that this is not a man-in-the-middle attack,\n" \ + "please remove this host from %s.\n" + +#define UNKNOWN_CERT_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>" \ + "<title>Unknown certificate</title>" \ + "<h1>Unknown certificate</h1>" \ + "<p>\n" \ + "The hostname of the server you are visiting could not be found\n" \ + "in your list of known hosts (%s).\n" \ + "<p>\n" \ + "The server has sent us a certificate with the following\n" \ + "fingerprint:\n" \ + "<pre>%s</pre>\n" \ + "<p>Trust it?\n" \ + "<form method=POST>" \ + "<input type=submit name=trust_cert value=always>\n" \ + "<input type=submit name=trust_cert value=once>" \ + "<input type=hidden name=entry value='%s sha256 %s %lu'>" \ + "</form>" + +#define UPDATED_CERT_RESPONSE "Content-Type: text/html\r\n" \ + "\r\n" \ + "<!DOCTYPE html>\n" \ + "<title>Certificate date changed</title>\n" \ + "<h1>Certificate date changed</h1>\n" \ + "<p>\n" \ + "The received certificate's date did not match the date in your\n" \ + "list of known hosts (%s).\n" \ + "<p>\n" \ + "The new expiration date is: %s.\n" \ + "<p>\n" \ + "Update it?\n" \ + "<form method=POST>" \ + "<input type=submit name=trust_cert value=always>" \ + "<input type=submit name=trust_cert value=once>\n" \ + "<input type=hidden name=entry value='%s sha256 %s %lu'>" \ + "</form>\n" + +#define PDIE(x) \ + do { \ + puts("Content-Type: text/plain\r\n"); \ + puts(x); \ + puts(strerror(errno)); \ + exit(1); \ + } while (0) + +#define SDIE(x) \ + do { \ + puts("Content-Type: text/plain\r\n"); \ + puts(x); \ + ERR_print_errors_fp(stdout); \ + exit(1); \ + } while (0) + +#define DIE(x) \ + do { \ + puts("Content-Type: text/plain\r\n\r\n" x); \ + exit(1); \ + } while (0) + +#define FLAGS (SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_COMPRESSION | \ + SSL_OP_NO_TLSv1_1) +#define PREFERRED_CIPHERS "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4" +#define BUFSIZE 1024 + +/* A larger buffer that we can use for storing the full public key. */ +#define BUFSIZE2 8192 + +static char buffer[BUFSIZE + 1]; +static char buffer2[BUFSIZE + 1]; +static char urlbuf[BUFSIZE + 1]; +static char khsbuf[BUFSIZE + 2]; +static unsigned char hashbuf[EVP_MAX_MD_SIZE]; +static char hashbuf2[EVP_MAX_MD_SIZE * 3 + 1]; +static FILE *known_hosts = NULL; + +static void setup_ssl(void) +{ + SSL_library_init(); + SSL_load_error_strings(); + ssl_ctx = SSL_CTX_new(TLS_client_method()); + SSL_CTX_set_options(ssl_ctx, FLAGS); + if (!(conn = BIO_new_ssl_connect(ssl_ctx))) + SDIE("Error creating BIO"); +} + +static void extract_hostname(const char *s, char **hostp, char **portp, + char **pathp, char **endp) +{ + const char *p; + size_t i, schlen; + + if (!(p = strstr(s, "gemini://"))) + DIE("Invalid URL: scheme delimiter not found"); + p += strlen("gemini://"); + schlen = p - s; + if (schlen >= BUFSIZE) + DIE("Scheme too long"); +#define SCHEME "gemini://" + schlen = strlen(SCHEME); + strcpy(urlbuf, SCHEME); + *hostp = &urlbuf[schlen]; + for (i = schlen; *p && *p != ':' && *p != '/' && i < BUFSIZE; ++p, ++i) + urlbuf[i] = *p; + if (i + 2 >= BUFSIZE) /* +2 for CRLF */ + DIE("Host too long"); + *portp = &urlbuf[i]; + if (*p != ':') { + if (i + 5 >= BUFSIZE) + DIE("Host too long"); + strcpy(&urlbuf[i], ":1965"); + i += 5; + } else { + for (; *p && *p != '/' && i < BUFSIZE; ++i, ++p) + urlbuf[i] = *p; + } + *pathp = &urlbuf[i]; + if (i < BUFSIZE) + urlbuf[i++] = '/'; + if (*p == '/') + ++p; + for (; *p && i < BUFSIZE; ++i, ++p) + urlbuf[i] = *p; + if (i + 2 >= BUFSIZE) /* +2 for CRLF */ + DIE("Host too long"); + *endp = &urlbuf[i]; + urlbuf[i] = '\0'; +} + +int check_cert(const char *theirs, char *linebuf, char *hostp, + char **stored_digestp, time_t their_time) +{ + char *p, *q, *hashp, *timep; + int found; + time_t our_time; + + rewind(known_hosts); + found = 0; + while (!found && fgets(linebuf, BUFSIZE, known_hosts)) { + p = strstr(linebuf, " "); + if (!p) + DIE("Incorrectly formatted known_hosts file"); + *p = '\0'; + found = !strcmp(linebuf, hostp); + } + if (!found) + return -1; + hashp = p + 1; + if (!(q = strstr(hashp, " "))) + DIE("Incorrectly formatted known_hosts file"); + *q = '\0'; + if (strcmp(hashp, "sha256") && strcmp(hashp, "SHA256")) + DIE("Unsupported digest format"); + *stored_digestp = q + 1; + if (!(q = strstr(*stored_digestp, " "))) { + timep = NULL; + if ((q = strstr(*stored_digestp, "\n"))) + *q = '\0'; + } else { + timep = q + 1; + *q = '\0'; + } + if (strcmp(theirs, *stored_digestp)) + return 0; + if (!timep) + return -2; + our_time = (time_t)atol(timep); + if (their_time != our_time) + return -2; + return 1; +} + +static char HexTable[] = "0123456789ABCDEF"; + +void hex_encode(const unsigned char *inp, char *outbuf, int len) +{ + const unsigned char *p; + char *q; + + for (p = inp, q = outbuf; p < &inp[len]; ++p) { + if (p != inp) + *q++ = ':'; + *q++ = HexTable[(*p >> 4) & 0xF]; + *q++ = HexTable[*p & 0xF]; + } + *q++ = '\0'; +} + +static void hash_buf(const unsigned char *ibuf, int len, unsigned char *obuf, + char *obuf2) +{ + unsigned int len2; + EVP_MD_CTX* mdctx; + + if (!(mdctx = EVP_MD_CTX_new())) + SDIE("Failed to initialize MD_CTX"); + if (!EVP_DigestInit_ex(mdctx, EVP_sha256(), NULL)) + SDIE("Failed to initialize sha256"); + if (!EVP_DigestUpdate(mdctx, ibuf, len)) + SDIE("Failed to update digest"); + len2 = 0; + if (!EVP_DigestFinal_ex(mdctx, obuf, &len2)) + SDIE("Failed to finalize digest"); + EVP_MD_CTX_free(mdctx); + hex_encode(obuf, obuf2, len2); +} + +/* 1: cert found & valid + * 0: cert found & invalid + * -1: cert not found + * -2: cert found, but notAfter updated + */ +static int connect(char *hostp, char *portp, char *pathp, char *endp, + char **stored_digestp, time_t *their_time) +{ + X509 *cert; + const EVP_PKEY *pkey; + unsigned char *pubkey_buf, *r; + int len, res; + const ASN1_TIME *notAfter; + struct tm their_tm; + + *pathp = '\0'; + if (!BIO_set_conn_hostname(conn, hostp)) + SDIE("Error setting BIO hostname"); + *pathp = '/'; + BIO_get_ssl(conn, &ssl); + if (!SSL_set_cipher_list(ssl, PREFERRED_CIPHERS)) + SDIE("Error failed to set cipher list"); + *portp = '\0'; + if (!SSL_set_tlsext_host_name(ssl, hostp)) + SDIE("Error failed to set tlsext host name"); + if (BIO_do_connect(conn) <= 0) + SDIE("Failed to connect"); + if (!BIO_do_handshake(conn)) + SDIE("Failed handshake"); + if (!(cert = SSL_get_peer_certificate(ssl))) + DIE("Failed to get certificate"); + if (!(pkey = X509_get0_pubkey(cert))) + SDIE("Failed to decode public key"); + len = i2d_PUBKEY(pkey, NULL); + if (len * 3 > BUFSIZE2) + DIE("Public key too long"); + pubkey_buf = (unsigned char *)buffer2; + r = pubkey_buf; + if (i2d_PUBKEY(pkey, &r) != len) + DIE("wat"); + hash_buf(pubkey_buf, len, hashbuf, hashbuf2); + notAfter = X509_get0_notAfter(cert); + if (!ASN1_TIME_to_tm(notAfter, &their_tm)) + DIE("Failed to parse time"); + if (X509_cmp_current_time(X509_get0_notBefore(cert)) >= 0) + DIE("Wrong time"); + if (X509_cmp_current_time(notAfter) <= 0) + DIE("Wrong time"); + *their_time = mktime(&their_tm); + res = check_cert(hashbuf2, buffer, hostp, stored_digestp, *their_time); + *portp = ':'; + X509_free(cert); + strcpy(endp, "\r\n"); + return res; +} + +static void read_response(void) +{ + int bytes, total; + const char *tmp; + char *q, status0, status1; + + /* Read response */ + total = 0; + /* Status code */ + while (((bytes = BIO_read(conn, buffer, 3 - total)) > 0 || + BIO_should_retry(conn)) && total < 3) + total += bytes; + if (total < 3 || !isdigit(status0 = buffer[0]) || + !isdigit(status1 = buffer[1]) || buffer[2] != ' ') + DIE("Invalid status code"); + /* Meta */ + #define METALEN (total - 3) + while (((bytes = BIO_read(conn, &buffer[METALEN], 1024 - METALEN)) > 0 || + BIO_should_retry(conn)) && METALEN < BUFSIZE) + total += bytes; + q = strstr(buffer, "\r\n"); + if (!q) + DIE("Invalid status line"); + *q = '\0'; + /* buffer is now META. */ + switch (status0) { + case '1': /* input */ + /* META is the prompt. */ + printf(INPUT_RESPONSE, urlbuf, buffer, status1 == '1' ? + "password" /* sensitive input */ : + "search" /* input */); + break; + case '2': /* success */ + /* META is the content type. */ + printf(SUCCESS_RESPONSE, *buffer ? + buffer : + "text/gemini; charset=utf-8" /* fallback */); + /* Body */ + /* flush any data remaining in buffer */ + total -= 5 + (q - buffer); /* code + space + meta + \r\n len */ + if (total > 0) + fwrite(&q[2], 1, total, stdout); + while ((bytes = BIO_read(conn, buffer, BUFSIZE)) > 0 || + BIO_should_retry(conn)) + fwrite(buffer, 1, bytes, stdout); + break; + case '3': /* redirect */ + /* META is the redirection URL. */ + printf(REDIRECT_RESPONSE, status1 == '0' ? + '7' /* temporary */ : + '1' /* permanent */, buffer); + break; + case '4': /* temporary failure */ + /* META is additional information. */ + /* TODO maybe set status code too? */ + switch (status1) { + case '1': + tmp = "Server unavailable"; + break; + case '2': + tmp = "CGI error"; + break; + case '3': + tmp = "Proxy error"; + break; + case '4': + tmp = "Slow down!"; + break; + case '0': + default: /* no additional information provided in the code */ + tmp = "Temporary failure"; + break; + } + printf(TEMPFAIL_RESPONSE, tmp, buffer); + break; + case '5': /* permanent failure */ + /* TODO maybe set status code too? */ + switch (status1) { + case '1': + tmp = "Not found"; + break; + case '2': + tmp = "Gone"; + break; + case '3': + tmp = "Proxy request refused"; + break; + case '9': + tmp = "Bad request"; + break; + case '0': + default: /* no additional information provided in the code */ + tmp = "Permanent failure"; + break; + } + printf(PERMFAIL_RESPONSE, tmp, buffer); + break; + case '6': /* permanent failure */ + /* TODO maybe set status code too? */ + switch (status1) { + case '1': + tmp = "Certificate not authorized"; + break; + case '2': + tmp = "Certificate not valid"; + break; + case '0': + default: /* no additional information provided in the code */ + tmp = "Certificate failure"; + break; + } + printf(CERTFAIL_RESPONSE, tmp, buffer); + } +} + +void decode_query(const char *input_url, char *output_buffer) +{ + const char *p; + char *q, *endp, c; + + endp = &output_buffer[BUFSIZE]; + for (p = input_url, q = output_buffer; *p && q < endp; ++p, ++q) { + if (*p != '%') { + *q = *p; + } else { + if (!isxdigit(p[1] & 0xFF) || !isxdigit(p[2] & 0xFF)) + DIE("Invalid percent encoding"); + c = tolower(p[1] & 0xFF); + *q = ('a' <= c && c <= 'z') ? + c - 'a' + 10 : + c - '0'; + c = tolower(p[2] & 0xFF); + *q = (*q << 4) | (('a' <= c || c <= 'z') ? + c - 'a' + 10 : + c - '0'); + p += 2; + } + } + if (q >= endp) + DIE("Query too long"); + *q = '\0'; +} + +void read_post(const char *hostp, char *portp, char *pathp) +{ + /* TODO move query strings here */ + size_t n; + char *p, *q; + FILE *known_hosts_tmp; + long last_pos, len, total; + size_t khslen; + + n = fread(buffer2, 1, BUFSIZE2, stdin); + buffer2[n] = '\0'; + if ((p = strstr(buffer2, "input="))) { + decode_query(p + 6, buffer); + if (!(q = strstr(pathp, "?"))) /* no query string */ + q = &pathp[strlen(pathp)]; + for (; *p && q < &urlbuf[BUFSIZE]; ++p, ++q) + *q = *p; + if (q >= &urlbuf[BUFSIZE]) + DIE("Query too long"); + } else if (!(p = strstr(buffer2, "trust_cert="))) { + DIE("Invalid POST request: trust_cert missing"); + } + p += sizeof("trust_cert=") - 1; + if (!strncmp(p, "always", 6)) { + /* move to file end */ + fseek(known_hosts, 0L, SEEK_END); + last_pos = ftell(known_hosts); + if (!(p = strstr(p, "entry="))) + DIE("Invalid POST request: missing entry"); + p += sizeof("entry=") - 1; + decode_query(p, buffer); + /* replace plus signs */ + p = buffer; + while ((p = strstr(p, "+"))) + *p = ' '; + fwrite(buffer, 1, strlen(buffer), known_hosts); + fwrite("\n", 1, 1, known_hosts); + khslen = strlen(khsbuf); + khsbuf[khslen] = '~'; + khsbuf[khslen + 1] = '\0'; + if (!(known_hosts_tmp = fopen(khsbuf, "w+"))) + PDIE("Error opening temporary hosts file"); + rewind(known_hosts); + *portp = '\0'; + total = 0; + while (fgets(buffer, BUFSIZE, known_hosts)) { + len = strlen(buffer); + if (!len) + continue; + if ((total += len) > last_pos) { + /* finished */ + fwrite(buffer, 1, len, known_hosts_tmp); + break; + } + if (buffer[len - 1] != '\n') { + /* clean up */ + fclose(known_hosts_tmp); + unlink(khsbuf); + DIE("Line too long"); + } + if (!(p = strstr(buffer, " "))) + DIE("Invalid entry in known_hosts file"); + *p = '\0'; + if (strcmp(buffer, hostp)) { + *p = ' '; + fwrite(buffer, 1, len, known_hosts_tmp); + } + } + *portp = ':'; + memcpy(buffer, khsbuf, BUFSIZE + 1); + buffer[khslen] = '\0'; + fclose(known_hosts); + fclose(known_hosts_tmp); + if (rename(khsbuf, buffer)) + PDIE("Failed to rename temporary file"); + khsbuf[khslen] = '\0'; + if (!(known_hosts = fopen(khsbuf, "a+"))) + PDIE("Failed to re-open known hosts file"); + } else if (strncmp(p, "once", 4)) { + DIE("Invalid POST request"); + } +} + +void open_known_hosts(void) +{ + const char *known_hosts_path, *xdg_dir, *home_dir; + char *p; + size_t len; + struct stat s; + + known_hosts_path = getenv("GMIFETCH_KNOWN_HOSTS"); + if (!known_hosts_path) { + xdg_dir = getenv("XDG_CONFIG_HOME"); + if ((xdg_dir = getenv("XDG_CONFIG_HOME"))) { + len = strlen(xdg_dir); +#define CONFIG_REL "/gmifetch/known_hosts" + if (len + sizeof(CONFIG_REL) > BUFSIZE) + DIE("Error: config directory path too long"); + memcpy(khsbuf, xdg_dir, len); + memcpy(&khsbuf[len], CONFIG_REL, sizeof(CONFIG_REL)); + } else { + if (!(home_dir = getenv("HOME"))) + home_dir = getpwuid(getuid())->pw_dir; + if (!home_dir) + DIE("Error: failed to get HOME directory"); +#undef CONFIG_REL +#define CONFIG_REL "/.config/gmifetch/known_hosts" + len = strlen(home_dir); + if (len + sizeof(CONFIG_REL) > BUFSIZE) + DIE("Error: home directory path too long"); + memcpy(khsbuf, home_dir, len); + memcpy(&khsbuf[len], CONFIG_REL, sizeof(CONFIG_REL)); + } + } else { + len = strlen(known_hosts_path); + if (len > BUFSIZE) + DIE("Error: known hosts path too long"); + memcpy(khsbuf, known_hosts_path, len); + } + p = khsbuf; + if (*p == '/') + ++p; + for (; *p; ++p) { + if (*p == '/') { + *p = '\0'; + if (stat(khsbuf, &s) == -1) { + if (errno != ENOENT) + PDIE("Error calling stat"); + if (mkdir(khsbuf, 0755) == -1) + PDIE("Error calling mkdir"); + } else if (!S_ISDIR(s.st_mode)) { + if (mkdir(khsbuf, 0755) == -1) + PDIE("Error calling mkdir"); + } + *p = '/'; + } + } + if (!(known_hosts = fopen(khsbuf, "a+"))) + PDIE("Error opening known hosts file"); +} + +int main(int argc, const char *argv[]) +{ + const char *input_url, *method; + char *hostp, *portp, *pathp, *endp, *stored_digestp; + int connect_res; + time_t their_time; + + if (argc != 2) { + input_url = getenv("QUERY_STRING"); + if (!input_url) + DIE("Usage: gmifetch [url] (or set QUERY_STRING)"); + decode_query(input_url, buffer); + input_url = buffer; + } else { + input_url = argv[1]; + } + open_known_hosts(); + setup_ssl(); + extract_hostname(input_url, &hostp, &portp, &pathp, &endp); + method = getenv("REQUEST_METHOD"); + if (method && !strcmp(method, "POST")) + read_post(hostp, portp, pathp); + connect_res = connect(hostp, portp, pathp, endp, &stored_digestp, + &their_time); + if (connect_res == 1) { /* valid certificate */ + BIO_puts(conn, urlbuf); + read_response(); + } else if (connect_res == 0) { /* invalid certificate */ + printf(INVALID_CERT_RESPONSE, stored_digestp, buffer, khsbuf); + } else if (connect_res == -1) { /* no certificate */ + *portp = '\0'; + printf(UNKNOWN_CERT_RESPONSE, khsbuf, hashbuf2, hostp, + hashbuf2, (unsigned long)their_time); + } else { /* -2: updated expiration date */ + *portp = '\0'; + printf(UPDATED_CERT_RESPONSE, khsbuf, + ctime(&their_time), hostp, hashbuf2, + (unsigned long)their_time); + } + BIO_free_all(conn); + exit(0); +} diff --git a/bonus/trans.cgi b/bonus/trans.cgi new file mode 100644 index 00000000..f40ed893 --- /dev/null +++ b/bonus/trans.cgi @@ -0,0 +1,13 @@ +#!/bin/sh +# Needs https://github.com/soimort/translate-shell to work. +# Usage: cgi-bin:trans.cgi?word + +TEXT="$(echo "$QUERY_STRING" | sed 's/+/ /g;s/%/\\x/g' | xargs -0 printf "%b")" +printf 'Content-Type: text/plain\n' + +type trans || { + printf "\n\nERROR: translator not found" + exit +} + +printf '\n%s\n' "$(trans "$TEXT")" |