diff options
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) |