diff options
author | bptato <nincsnevem662@gmail.com> | 2024-07-13 19:54:39 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-10-12 16:30:11 +0200 |
commit | 6087f759642cd747d03e43cc57da0a508ccc1d61 (patch) | |
tree | 3c3261e0d28fd72003e50623b64f80ce22a803ff /adapter | |
parent | 47caa59e5d1455484c4e5b08f58177a1b2172f44 (diff) | |
download | chawan-6087f759642cd747d03e43cc57da0a508ccc1d61.tar.gz |
gmifetch: rewrite in Nim
This finally makes it possible to use socks5 for Gemini. Also slightly refactored the config, to make it easier to pass on the config dir. By the way, the known_hosts file is now stored in the config dir too. The adapter will try to move it to there from the old location.
Diffstat (limited to 'adapter')
-rw-r--r-- | adapter/protocol/gemini.nim | 358 | ||||
-rw-r--r-- | adapter/protocol/gmifetch.c | 648 | ||||
-rw-r--r-- | adapter/protocol/lcgi.nim | 24 | ||||
-rw-r--r-- | adapter/protocol/lcgi_ssl.nim | 119 |
4 files changed, 491 insertions, 658 deletions
diff --git a/adapter/protocol/gemini.nim b/adapter/protocol/gemini.nim new file mode 100644 index 00000000..75ddf5cb --- /dev/null +++ b/adapter/protocol/gemini.nim @@ -0,0 +1,358 @@ +import std/options +import std/os +import std/posix +import std/strutils + +import lcgi_ssl + +proc sdie(s: string) = + stdout.write("Cha-Control: ConnectionError 5 " & s & ": ") + ERR_print_errors_fp(stdout) + stdout.flushFile() + quit(1) + +proc fopen(filename, mode: cstring): pointer {.importc, nodecl.} +proc openKnownHosts(os: PosixStream): (File, string) = + var path = getEnv("GMIFETCH_KNOWN_HOSTS") + if path == "": + let oldPath = getConfigDir() & "gmifetch/known_hosts" + let ourDir = getEnv("CHA_CONFIG_DIR") + if ourDir == "": + os.die("InternalError", "config dir missing") + path = ourDir & '/' & "gemini_known_hosts" + # for backwards compat, TODO eventually remove this + # (this has a race, but oh well.) + if fileExists(oldPath) and not fileExists(path): + moveFile(oldPath, path) + createDir(path.beforeLast('/')) + let f = cast[File](fopen(cstring(path), "a+")) + if f == nil: + os.die("InternalError", "error opening open known hosts file") + return (f, path) + +proc readPost(os: PosixStream; query: var string; host, knownHostsPath: string; + knownHosts: var File; tmpEntry: var string) = + let s = newPosixStream(STDIN_FILENO).recvAll() + if (var i = s.find("input="); i != -1): + i += "input=".len + query = s.toOpenArray(i, s.high).percentDecode() + elif (var i = s.find("trust_cert="); i != -1): + i += "trust_cert=".len + let t = s.until('&', i) + if t in ["always", "yes", "no", "once"]: + i = s.find("entry=", i) + if i == -1: + os.die("InternalError", "missing entry field in POST") + i += "entry=".len + var buf = "" + for i in i ..< s.len: + if s[i] == '+': + buf &= ' ' + else: + buf &= s[i] + buf = buf.percentDecode() + if t == "once" or t == "no": + tmpEntry = buf + else: + var knownHostsTmp: File + let knownHostsTmpPath = knownHostsPath & '~' + if not knownHostsTmp.open(knownHostsTmpPath, fmWrite): + os.die("InternalError", "failed to open temp file") + var line: string + while knownHosts.readLine(line): + let j = line.find(' ') + if host.len == j and line.startsWith(host): + continue # delete this entry + knownHostsTmp.writeLine(line) + knownHostsTmp.writeLine(buf) + knownHostsTmp.close() + knownHosts.close() + try: + moveFile(knownHostsTmpPath, knownHostsPath) + except IOError: + os.die("InternalError failed to move tmp file") + if not knownHosts.open(knownHostsPath, fmRead): + os.die("InternalError", "failed to reopen known_hosts") + else: + os.die("InternalError invalid POST: wrong trust_cert") + else: + os.die("InternalError invalid POST: no input or trust_cert") + +type CheckCertResult = enum + ccrNotFound, ccrNewExpiration, ccrFoundInvalid, ccrFoundValid + +proc checkCert(os: PosixStream; theirDigest, host: string; + storedDigest: var string; theirTime: var Time; knownHosts: File; + tmpEntry: string): CheckCertResult = + var line = tmpEntry + var found = line.until(' ') == host + while not found and knownHosts.readLine(line): + found = line.until(' ') == host + if not found: + return ccrNotFound + let ss = line.split(' ') + if ss.len < 3: + os.die("InternalError", "wrong line in known_hosts file") + if ss[1] != "sha256": + os.die("InternalError", "unsupported digest format in known_hosts file") + storedDigest = ss[2] + if storedDigest != theirDigest: + return ccrFoundInvalid + if ss.len > 3: + if (let x = parseUInt64(ss[3], allowSign = false); x.isSome): + if Time(x.get) == theirTime: + return ccrFoundValid + else: + os.die("InternalError", "invalid time in known_hosts file") + return ccrNewExpiration + +proc hashBuf(ibuf: openArray[uint8]): string = + const HexTable = "0123456789ABCDEF" + var len2: cuint = 0 + var buf = newSeq[char](EVP_MAX_MD_SIZE) + let mdctx = EVP_MD_CTX_new() + if mdctx == nil: + sdie("failed to initialize MD_CTX") + if EVP_DigestInit_ex(mdctx, EVP_sha256(), nil) == 0: + sdie("failed to initialize sha256") + if EVP_DigestUpdate(mdctx, unsafeAddr ibuf[0], cuint(ibuf.len)) == 0: + sdie("failed to update digest") + if EVP_DigestFinal_ex(mdctx, addr buf[0], len2) == 0: + sdie("failed to finalize digest") + EVP_MD_CTX_free(mdctx); + # hex encode buf + result = "" + for i in 0 ..< int(len2): + if i != 0: + result &= ':' + let u = uint8(buf[i]) + result &= HexTable[(u shr 4) and 0xF] + result &= HexTable[u and 0xF] + +proc connect(os: PosixStream; ssl: ptr SSL; host, port: string; + knownHosts: File; storedDigest, theirDigest: var string; + theirTime: var Time; tmpEntry: string): CheckCertResult = + let hostname = host & ':' & port + discard SSL_set1_host(ssl, cstring(hostname)) + if SSL_connect(ssl) <= 0: + sdie("failed to connect") + if SSL_do_handshake(ssl) <= 0: + sdie("failed handshake") + let cert = SSL_get0_peer_certificate(ssl) + if cert == nil: + sdie("failed to get peer certificate") + let pkey = X509_get0_pubkey(cert) + if pkey == nil: + sdie("failed to decode public key") + var pubkeyBuf: array[16384, uint8] + let len = i2d_PUBKEY(pkey, nil); + if len * 3 > pubkeyBuf.len: + os.die("InternalError", "pubkey too long") + var r = addr pubkeyBuf[0] + if i2d_PUBKEY(pkey, addr r) != len: + os.die("InternalError", "wat") + theirDigest = pubkeyBuf.toOpenArray(0, len - 1).hashBuf() + let notAfter = X509_get0_notAfter(cert) + var theirTm: Tm + if ASN1_TIME_to_tm(notAfter, addr theirTm) == 0: + sdie("Failed to parse time"); + if getEnv("CHA_INSECURE_SSL_NO_VERIFY") != "1": + if X509_cmp_current_time(X509_get0_notBefore(cert)) >= 0 or + X509_cmp_current_time(notAfter) <= 0: + os.die("InvalidResponse", "received an expired certificate"); + theirTime = mktime(theirTm) + return os.checkCert(theirDigest, host, storedDigest, theirTime, knownHosts, + tmpEntry) + +proc readResponse(os: PosixStream; ssl: ptr SSL; reqBuf: string) = + var buffer = newString(4096) + var n = 0 + while n < buffer.len: + let m = SSL_read(ssl, addr buffer[n], cint(buffer.len - n)) + if m == 0: + break + n += m + let status0 = buffer[0] + let status1 = buffer[1] + if status0 notin AsciiDigit or status1 notin AsciiDigit: + os.die("InvalidResponse", "invalid status code") + while n < 1024 + 3: # max meta len is 1024 + let m = SSL_read(ssl, addr buffer[n], cint(buffer.len - n)) + if m == 0: + break + n += m + let i = buffer.find("\r\n") + if i == -1: + os.die("InvalidResponse", "invalid status line") + var meta = buffer.substr(3, i - 1) + if '\n' in meta: + os.die("InvalidResponse", "invalid status line") + case status0 + of '1': # input + # META is the prompt. + let it = if status1 == '1': "password" else: "search" + os.sendDataLoop("""Content-Type: text/html + +<!DOCTYPE html> +<title>Input required</title> +<base href='""" & reqBuf.htmlEscape() & """'> +<h1>Input required</h1> +<p> +""" & meta.htmlEscape() & """ +<p> +<form method=POST><input type='""" & it & """' name='input'></form> +""") + of '2': # success + # META is the content type. + if meta == "": + meta = "text/gemini" + os.sendDataLoop("Content-Type: " & meta & "\n\n") + os.sendDataLoop(buffer.toOpenArray(i + 2, buffer.high)) + while true: + let n = SSL_read(ssl, addr buffer[0], cint(buffer.len)) + if n == 0: + break + os.sendDataLoop(buffer.toOpenArray(0, int(n) - 1)) + of '3': # redirect + # META is the redirection URL. + let c = if status1 == '0': + '7' # temporary + else: + '1' # permanent + os.sendDataLoop("Status: 30" & c & "\nLocation: " & meta & "\n\n") + of '4': # temporary failure + # META is additional information. + let tmp = case status1 + of '1': "Server unavailable" + of '2': "CGI error" + of '3': "Proxy error" + of '4': "Slow down!" + else: "Temporary failure" # no additional information provided in the code + os.sendDataLoop("""Content-Type: text/html + +<!DOCTYPE html> +<title>Temporary failure</title> +<h1>""" & tmp & """</h1> +<p> +""" & meta.htmlEscape()) + of '5': # permanent failure + # META is additional information. + let tmp = case status1 + of '1': "Not found" + of '2': "Gone" + of '3': "Proxy request refused" + of '4': "Bad request" + else: "Permanent failure" + os.sendDataLoop("""Content-Type: text/html + +<!DOCTYPE html> +<title>Permanent failure</title> +<h1>""" & tmp & """</h1> +<p> +""" & meta.htmlEscape()) + of '6': # certificate failure + # META is additional information. + let tmp = case status1 + of '1': "Certificate not authorized" + of '2': "Certificate not valid" + else: "Certificate failure" + os.sendDataLoop("""Content-Type: text/html + +<!DOCTYPE html> +<title>Certificate failure</title> +<h1>""" & tmp & """</h1> +<p> +""" & meta.htmlEscape()) + else: + os.die("InvalidResponse", "Wrong status code") + +proc main() = + let os = newPosixStream(STDOUT_FILENO) + let host = getEnv("MAPPED_URI_HOST") + var (knownHosts, knownHostsPath) = os.openKnownHosts() + var port = getEnv("MAPPED_URI_PORT") + if port == "": + port = "1965" + var path = getEnv("MAPPED_URI_PATH") + if path == "": + path = "/" + var reqBuf = "gemini://" & host & path + var query = getEnv("MAPPED_URI_QUERY") + var tmpEntry = "" # for accepting a self signed cert "once" + if getEnv("REQUEST_METHOD") == "POST": + os.readPost(query, host, knownHostsPath, knownHosts, tmpEntry) + if query != "": + reqBuf &= '?' & query + reqBuf &= "\r\n" + let ssl = os.connectSSLSocket(host, port) + var storedDigest: string + var theirDigest: string + var theirTime: Time + case os.connect(ssl, host, port, knownHosts, storedDigest, theirDigest, + theirTime, tmpEntry) + of ccrFoundValid: + discard SSL_write(ssl, cstring(reqBuf), cint(reqBuf.len)) + os.readResponse(ssl, reqBuf) + of ccrFoundInvalid: + os.sendDataLoop(""" +Content-Type: text/html + +<!DOCTYPE html> +<title>Invalid certificate</title> +<h1>Invalid certificate</h1> +<p> +The certificate received from the server does not match the +stored certificate (expected """ & storedDigest & """, but got +""" & theirDigest & """). Somebody may be tampering with your +connection. +<p> +If you are sure that this is not a man-in-the-middle attack, +please remove this host from """ & knownHostsPath & """. +""") + of ccrNotFound: + os.sendDataLoop(""" +Content-Type: text/html + +<!DOCTYPE html> +<title>Unknown certificate</title> +<h1>Unknown certificate</h1> +<p> +The hostname of the server you are visiting could not be found +in your list of known hosts (""" & knownHostsPath & """). +<p> +The server has sent us a certificate with the following +fingerprint: +<pre>""" & theirDigest & """</pre> +<p> +Trust it? +<form method=POST> +<input type=submit name=trust_cert value=always> +<input type=submit name=trust_cert value=once> +<input type=hidden name=entry value='""" & + host & " sha256 " & theirDigest & " " & $uint64(theirTime) & """'> +</form> +""") + of ccrNewExpiration: + os.sendDataLoop(""" +Content-Type: text/html + +<!DOCTYPE html> +<title>Certificated date changed</title> +<h1>Certificated date changed</h1> +<p> +The received certificate's date did not match the date in your +list of known hosts (""" & knownHostsPath & """). +<p> +The new expiration date is: """ & ($ctime(theirTime)).strip() & """. +<p> +Update it? +<form method=POST> +<input type=submit name=trust_cert value=yes> +<input type=submit name=trust_cert value=no> +<input type=hidden name=entry value='""" & + host & " sha256 " & theirDigest & " " & $uint64(theirTime) & """'> +</form> +""") + closeSSLSocket(ssl) + +main() diff --git a/adapter/protocol/gmifetch.c b/adapter/protocol/gmifetch.c deleted file mode 100644 index bdad18e5..00000000 --- a/adapter/protocol/gmifetch.c +++ /dev/null @@ -1,648 +0,0 @@ -/* 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). - * - * Usage: gmifetch - * - * Environment variables: - * - MAPPED_URI_SCHEME, MAPPED_URI_HOST, MAPPED_URI_PORT, MAPPED_URI_PATH, - * MAPPED_URI_QUERY for the URL parts. (Parameters are ignored, gmifetch does - * not parse URLs.) - * - 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. (TODO: add a way to set this - * in config.toml) - */ - -#include <ctype.h> -#include <errno.h> -#include <openssl/err.h> -#include <openssl/pem.h> -#include <openssl/ssl.h> -#include <pwd.h> -#include <string.h> -#include <sys/stat.h> -#include <unistd.h> - -/* 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 { \ - fputs("Cha-Control: ConnectionError 1 " x ": ", stdout); \ - puts(strerror(errno)); \ - exit(1); \ - } while (0) - -#define SDIE(x) \ - do { \ - fputs("Cha-Control: ConnectionError 5 " x ": ", stdout); \ - ERR_print_errors_fp(stdout); \ - exit(1); \ - } while (0) - -#define DIE(x) \ - do { \ - puts("Cha-Control: ConnectionError 1 " x); \ - exit(1); \ - } while (0) - -#define BUFSIZE 1024 -#define PUBKEY_BUF_SIZE 8192 - -static int check_cert(const char *theirs, const char *hostp, - char **stored_digestp, char *linebuf, time_t their_time, - FILE *known_hosts) -{ - 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 void hash_buf(const unsigned char *ibuf, int len, char *obuf2) -{ - static const char HexTable[] = "0123456789ABCDEF"; - unsigned int len2 = 0; - EVP_MD_CTX* mdctx; - unsigned char hashbuf[EVP_MAX_MD_SIZE]; - const unsigned char *p; - char *q; - - 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"); - if (!EVP_DigestFinal_ex(mdctx, hashbuf, &len2)) - SDIE("Failed to finalize digest"); - EVP_MD_CTX_free(mdctx); - /* hex encode hashbuf */ - for (p = hashbuf, q = obuf2; p < &hashbuf[len2]; ++p) { - if (p != hashbuf) - *q++ = ':'; - *q++ = HexTable[(*p >> 4) & 0xF]; - *q++ = HexTable[*p & 0xF]; - } - *q = '\0'; -} - -/* 1: cert found & valid - * 0: cert found & invalid - * -1: cert not found - * -2: cert found, but notAfter updated - */ -static int connect(BIO *conn, const char *hostp, const char *portp, - char *linebuf, char **stored_digestp, time_t *their_time, - char *hashbuf2, FILE *known_hosts) -{ - X509 *cert; - const EVP_PKEY *pkey; - unsigned char pubkey_buf[PUBKEY_BUF_SIZE + 1], *r; - int len, res; - const ASN1_TIME *notAfter; - struct tm their_tm; - SSL *ssl; - - if (!BIO_set_conn_hostname(conn, hostp)) - SDIE("Error setting BIO hostname"); - if (!BIO_set_conn_port(conn, portp)) - SDIE("Error setting BIO port"); - BIO_get_ssl(conn, &ssl); -#define PREFERRED_CIPHERS "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4" - if (!SSL_set_cipher_list(ssl, PREFERRED_CIPHERS)) - SDIE("Error failed to set cipher list"); - 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 > PUBKEY_BUF_SIZE) - DIE("Public key too long"); - r = pubkey_buf; - if (i2d_PUBKEY(pkey, &r) != len) - DIE("wat"); - hash_buf(pubkey_buf, len, 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, hostp, stored_digestp, linebuf, *their_time, - known_hosts); - X509_free(cert); - return res; -} - -static void read_response(BIO *conn, const char *urlbuf) -{ - int bytes, total; - const char *tmp; - char *q, status0, status1; - char buffer[BUFSIZE + 1]; - - /* 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); - } -} - -static void decode_query(const char *input_url, char *output_buffer) -{ - const char *p; - char *q, 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'; -} - -static void read_post(const char *hostp, char *reqbuf, char *khsbuf, - FILE **known_hosts) -{ - size_t n; - char *p, *q; - FILE *known_hosts_tmp; - long last_pos, len, total; - size_t khslen; - char inbuf[BUFSIZE + 1], buffer[BUFSIZE + 1]; - - n = fread(inbuf, 1, BUFSIZE, stdin); - inbuf[n] = '\0'; - if ((p = strstr(inbuf, "input="))) { - p += strlen("input="); - decode_query(p, buffer); - if (!(q = strstr(reqbuf, "?"))) { /* no query string */ - q = &reqbuf[strlen(reqbuf)]; - if (q == &reqbuf[BUFSIZE]) - DIE("Query too long"); - *q++ = '?'; - } - for (; *p && q < &reqbuf[BUFSIZE]; ++p, ++q) - *q = *p; - if (q >= &reqbuf[BUFSIZE]) - DIE("Query too long"); - return; - } else if (!(p = strstr(inbuf, "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); - 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); - } - } - 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"); - } -} - -static FILE *open_known_hosts(char *khsbuf) -{ - const char *known_hosts_path, *xdg_dir, *home_dir; - char *p; - size_t len; - struct stat s; - FILE *known_hosts; - - 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"); - return known_hosts; -} - -int main(void) -{ - const char *schemep = getenv("MAPPED_URI_SCHEME"); - const char *hostp = getenv("MAPPED_URI_HOST"); - const char *portp = getenv("MAPPED_URI_PORT"); - const char *pathp = getenv("MAPPED_URI_PATH"); - const char *queryp = getenv("MAPPED_URI_QUERY"); - const char *method = getenv("REQUEST_METHOD"); - const char *all_proxy = getenv("ALL_PROXY"); - char *stored_digestp; - time_t their_time; - char hashbuf2[EVP_MAX_MD_SIZE * 3 + 1]; - char reqbuf[BUFSIZE + 1] = "gemini://", khsbuf[BUFSIZE + 2], - linebuf[BUFSIZE + 1]; - FILE *known_hosts; - SSL_CTX *ssl_ctx; - BIO *conn; - -#define PROXY_ERR "gmifetch does not support proxies yet. Please disable" \ - "your proxy for gemini URLs if you wish to proceed anyway." - if (all_proxy && *all_proxy) - DIE(PROXY_ERR); - known_hosts = open_known_hosts(khsbuf); - /* setup SSL */ - OPENSSL_init_ssl(0, NULL); - ssl_ctx = SSL_CTX_new(TLS_client_method()); - SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_2_VERSION); - /* check received URL */ - if (!(conn = BIO_new_ssl_connect(ssl_ctx))) - SDIE("Error creating BIO"); - if (!schemep || !*schemep || strcmp(schemep, "gemini")) - DIE("Invalid scheme"); - if (!hostp || !*hostp) - DIE("Missing hostname"); - if (!portp || !*portp) - portp = "1965"; - if (!pathp || !*pathp) - pathp = "/"; - if (!queryp) - queryp = ""; - /* Note: we do not include the port number in the request string, - * otherwise some servers refuse to serve anything. - * - * (I really wish this was explicitly mentioned in the standard. - * Something like: - * - * WARNING: some gemini servers will not accept URLs containing the - * default port number!!!) - */ -#define CAT(me, ow) strncat(me, ow, sizeof(me) - strlen(me) - 1); - CAT(reqbuf, hostp); - CAT(reqbuf, pathp); - if (*queryp) { - CAT(reqbuf, "?"); - CAT(reqbuf, queryp); - } - /* read_post may modify reqbuf (it appends a query string for input form - * responses) */ - if (method && !strcmp(method, "POST")) - read_post(hostp, reqbuf, khsbuf, &known_hosts); - CAT(reqbuf, "\r\n"); - switch (connect(conn, hostp, portp, linebuf, &stored_digestp, - &their_time, hashbuf2, known_hosts)) - { - case 1: /* valid certificate, connect */ - BIO_puts(conn, reqbuf); - read_response(conn, reqbuf); - break; - case 0: /* invalid certificate */ - printf(INVALID_CERT_RESPONSE, stored_digestp, hashbuf2, - khsbuf); - break; - case -1: /* no certificate */ - printf(UNKNOWN_CERT_RESPONSE, khsbuf, hashbuf2, hostp, - hashbuf2, (unsigned long)their_time); - break; - case -2: /* -2: updated expiration date */ - printf(UPDATED_CERT_RESPONSE, khsbuf, - ctime(&their_time), hostp, hashbuf2, - (unsigned long)their_time); - break; - default: DIE("wat 2"); - } - BIO_free_all(conn); - exit(0); -} diff --git a/adapter/protocol/lcgi.nim b/adapter/protocol/lcgi.nim index e33f0609..89932ba9 100644 --- a/adapter/protocol/lcgi.nim +++ b/adapter/protocol/lcgi.nim @@ -11,8 +11,12 @@ export twtstr export STDIN_FILENO, STDOUT_FILENO -proc die*(os: PosixStream; s: string) = - os.sendDataLoop("Cha-Control: ConnectionError " & s) +proc die*(os: PosixStream; name: string; s = "") = + var buf = "Cha-Control: ConnectionError " & name + if s != "": + buf &= ' ' & s + buf &= '\n' + os.sendDataLoop(buf) quit(1) proc openSocket(os: PosixStream; host, port, resFail, connFail: string; @@ -28,10 +32,10 @@ proc openSocket(os: PosixStream; host, port, resFail, connFail: string; if err == 0: break if err < 0: - os.die(resFail & ' ' & $gai_strerror(err)) + os.die(resFail, $gai_strerror(err)) let sock = socket(res.ai_family, res.ai_socktype, res.ai_protocol) if cint(sock) < 0: - os.die("InternalError could not open socket") + os.die("InternalError", "could not open socket") return sock proc connectSocket(os: PosixStream; host, port, resFail, connFail: string; @@ -49,19 +53,19 @@ proc connectSocket(os: PosixStream; host, port, resFail, connFail: string; proc authenticateSocks5(os, ps: PosixStream; buf: array[2, uint8]; user, pass: string) = if buf[0] != 5: - os.die("ProxyInvalidResponse wrong socks version") + os.die("ProxyInvalidResponse", "wrong socks version") case buf[1] of 0x00: discard # no auth of 0x02: if user.len > 255 or pass.len > 255: - os.die("InternalError username or password too long") + os.die("InternalError", "username or password too long") let sbuf = "\x01" & char(user.len) & user & char(pass.len) & pass ps.sendDataLoop(sbuf) var rbuf = default(array[2, uint8]) ps.recvDataLoop(rbuf) if rbuf[0] != 1: - os.die("ProxyInvalidResponse wrong auth version") + os.die("ProxyInvalidResponse", "wrong auth version") if rbuf[1] != 0: os.die("ProxyAuthFail") of 0xFF: @@ -71,11 +75,11 @@ proc authenticateSocks5(os, ps: PosixStream; buf: array[2, uint8]; proc sendSocks5Domain(os, ps: PosixStream; host, port: string) = if host.len > 255: - os.die("InternalError host too long to send to proxy") + os.die("InternalError", "host too long to send to proxy") let dstaddr = "\x03" & char(host.len) & host let x = parseUInt16(port) if x.isNone: - os.die("InternalError wrong port") + os.die("InternalError", "wrong port") let port = x.get let sbuf = "\x05\x01\x00" & dstaddr & char(port shr 8) & char(port and 0xFF) ps.sendDataLoop(sbuf) @@ -121,7 +125,7 @@ proc connectProxySocket(os: PosixStream; host, port, proxy: string; let scheme = proxy.until(':') # We always use socks5h, actually. if scheme != "socks5" and scheme != "socks5h": - os.die("InternalError only socks5 proxy is supported") + os.die("InternalError", "only socks5 proxy is supported") var i = scheme.len + 1 while i < proxy.len and proxy[i] == '/': inc i diff --git a/adapter/protocol/lcgi_ssl.nim b/adapter/protocol/lcgi_ssl.nim new file mode 100644 index 00000000..b094bd4c --- /dev/null +++ b/adapter/protocol/lcgi_ssl.nim @@ -0,0 +1,119 @@ +import std/posix + +import lcgi + +export lcgi, dynstream, twtstr + +const libssl = staticExec("pkg-config --libs --silence-errors libssl libcrypto") + +{.passc: libssl.} +{.passl: libssl.} + +type + ASN1_TIME* = pointer + EVP_PKEY* = pointer + EVP_MD_CTX* = pointer + EVP_MD* = pointer + ENGINE* = pointer + +type + SSL_CTX* {.importc, header: "<openssl/types.h>", incompleteStruct.} = object + BIO* {.importc, header: "<openssl/types.h>", incompleteStruct.} = object + SSL* {.importc, header: "<openssl/types.h>", incompleteStruct.} = object + X509* {.importc, header: "<openssl/types.h>", incompleteStruct.} = object + +{.push importc.} + +let EVP_MAX_MD_SIZE* {.nodecl, header: "<openssl/evp.h>".}: cint + +{.push cdecl.} +{.push header: "<openssl/err.h>".} +proc ERR_print_errors_fp*(fp: File) +{.pop.} + +{.push header: "<openssl/x509.h>".} +proc X509_get0_pubkey*(x: ptr X509): EVP_PKEY +proc X509_get0_notAfter*(x: ptr X509): ASN1_TIME +proc X509_get0_notBefore*(x: ptr X509): ASN1_TIME +proc X509_cmp_current_time*(asn1_time: ASN1_TIME): cint + +proc i2d_PUBKEY*(a: EVP_PKEY; pp: ptr ptr uint8): cint +{.pop.} + +{.push header: "<openssl/evp.h>".} +proc EVP_MD_CTX_new*(): EVP_MD_CTX +proc EVP_MD_CTX_free*(ctx: EVP_MD_CTX) +proc EVP_DigestInit_ex*(ctx: EVP_MD_CTX; t: EVP_MD; impl: ENGINE): cint +proc EVP_DigestUpdate*(ctx: EVP_MD_CTX; d: pointer; cnt: csize_t): cint +proc EVP_DigestFinal_ex*(ctx: EVP_MD_CTX; md: ptr char; s: var cuint): cint + +proc EVP_sha256*(): EVP_MD +{.pop.} + +{.push header: "<openssl/asn1.h>".} +proc ASN1_TIME_to_tm*(s: ASN1_TIME; tm: ptr Tm): cint +{.pop.} + +{.push header: "<openssl/bio.h>", header: "<openssl/ssl.h>".} +proc BIO_get_ssl*(b: ptr BIO; sslp: var ptr SSL): clong +proc BIO_new_ssl_connect*(ctx: ptr SSL_CTX): ptr BIO +proc BIO_do_handshake*(b: ptr BIO): clong +{.pop.} + +{.push header: "<openssl/bio.h>".} +proc BIO_new_socket*(sock, close_flag: cint): ptr BIO +proc BIO_set_conn_hostname*(b: ptr BIO; name: cstring): clong +proc BIO_do_connect*(b: ptr BIO): clong +proc BIO_read*(b: ptr BIO; data: pointer; dlen: cint): cint +proc BIO_write*(b: ptr BIO; data: cstring; dlen: cint): cint + +proc BIO_should_retry*(b: ptr BIO): cint +{.pop.} + +{.push header: "<openssl/ssl.h>".} + +type SSL_METHOD* {.incompleteStruct.} = object + +let TLS1_2_VERSION* {.nodecl, header: "<openssl/ssl.h>"}: cint + +proc SSL_CTX_new*(m: ptr SSL_METHOD): ptr SSL_CTX +proc SSL_CTX_free*(ctx: ptr SSL_CTX) +proc SSL_get_SSL_CTX*(ssl: ptr SSL): ptr SSL_CTX +proc SSL_new*(ctx: ptr SSL_CTX): ptr SSL +proc TLS_client_method*(): ptr SSL_METHOD +proc SSL_CTX_set_min_proto_version*(ctx: ptr SSL_CTX; version: cint): cint +proc SSL_CTX_set_cipher_list*(ctx: ptr SSL_CTX; str: cstring): cint +proc SSL_get0_peer_certificate*(ssl: ptr SSL): ptr X509 +proc SSL_connect*(ssl: ptr SSL): cint +proc SSL_do_handshake*(ssl: ptr SSL): cint +proc SSL_set1_host*(ssl: ptr SSL; hostname: cstring): cint +proc SSL_read*(ssl: ptr SSL; buf: pointer; num: cint): cint +proc SSL_write*(ssl: ptr SSL; buf: pointer; num: cint): cint +proc SSL_set_fd*(ssl: ptr SSL; fd: cint): cint +proc SSL_shutdown*(ssl: ptr SSL): cint +proc SSL_free*(ssl: ptr SSL) + +{.pop.} # <openssl/ssl.h> + +{.pop.} # cdecl + +{.pop.} # importc + +proc connectSSLSocket*(os: PosixStream; host, port: string): ptr SSL = + let ps = os.connectSocket(host, port) + let ctx = SSL_CTX_new(TLS_client_method()) + if ctx.SSL_CTX_set_min_proto_version(TLS1_2_VERSION) == 0: + os.die("InternalError", "failed to set min proto version") + const preferredCiphers = "HIGH:!aNULL:!kRSA:!PSK:!SRP:!MD5:!RC4:!DSS" + if ctx.SSL_CTX_set_cipher_list(preferredCiphers) == 0: + os.die("InternalError", "failed to set cipher list") + let ssl = SSL_new(ctx) + if SSL_set_fd(ssl, ps.fd) == 0: + os.die("InternalError", "failed to set SSL fd") + return ssl + +proc closeSSLSocket*(ssl: ptr SSL) = + let ctx = SSL_get_SSL_CTX(ssl) + discard SSL_shutdown(ssl) + SSL_free(ssl) + SSL_CTX_free(ctx) |