~sircmpwn/gmni-discuss

gmnisrv: gmnisrv reverse proxy v1 PROPOSED

~avr: 2
 reverse proxy, based on gmni (https://git.sr.ht/~sircmpwn/gmni)
 removed sloppy comment

 12 files changed, 1148 insertions(+), 29 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~sircmpwn/gmni-discuss/patches/19966/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH gmnisrv 1/2] reverse proxy, based on gmni (https://git.sr.ht/~sircmpwn/gmni) Export this patch

From: Andreas R <avr@geminet.org>

---
 configure             |   4 +-
 doc/gmnisrvini.scd    |   6 +
 include/config.h      |   1 +
 include/proxy/proxy.h | 174 +++++++++++++
 include/proxy/tofu.h  |  49 ++++
 include/util.h        |   7 +-
 src/config.c          |   1 +
 src/proxy.c           | 584 ++++++++++++++++++++++++++++++++++++++++++
 src/serve.c           |  81 +++++-
 src/tofu.c            | 232 +++++++++++++++++
 src/util.c            |  17 ++
 11 files changed, 1148 insertions(+), 8 deletions(-)
 create mode 100644 include/proxy/proxy.h
 create mode 100644 include/proxy/tofu.h
 create mode 100644 src/proxy.c
 create mode 100644 src/tofu.c

diff --git a/configure b/configure
index d3fe645..9848397 100755
--- a/configure
+++ b/configure
@@ -15,7 +15,9 @@ gmnisrv() {
		src/server.c \
		src/tls.c \
		src/url.c \
		src/util.c
		src/util.c \
		src/proxy.c \
		src/tofu.c
}

all="gmnisrv"
diff --git a/doc/gmnisrvini.scd b/doc/gmnisrvini.scd
index 69e8129..00da78f 100644
--- a/doc/gmnisrvini.scd
+++ b/doc/gmnisrvini.scd
@@ -123,6 +123,12 @@ Within each routing section, the following keys are used to configure how
	"on" to enable CGI support. *root* must also be configured. See "CGI
	Support" for details.

*proxy*
	"[gemini://][URL][:PORT]" to indicate the source to serve as a reverse 
	proxy for. If [URL] or [:PORT] are not present, values of the original 
	request are used.
	

# CGI Support

*gmnisrv* supports a limited version of CGI, compatible with the Jetforce
diff --git a/include/config.h b/include/config.h
index f893b20..45f032d 100644
--- a/include/config.h
+++ b/include/config.h
@@ -28,6 +28,7 @@ struct gmnisrv_route {
	char *root;
	char *index;
	char *rewrite;
        char *proxy;
	bool autoindex;
	bool cgi;

diff --git a/include/proxy/proxy.h b/include/proxy/proxy.h
new file mode 100644
index 0000000..66986df
--- /dev/null
+++ b/include/proxy/proxy.h
@@ -0,0 +1,174 @@
#ifndef GEMINI_CLIENT_H
#define GEMINI_CLIENT_H
#include <netdb.h>
#include <openssl/ssl.h>
#include <stdbool.h>
#include <sys/socket.h>
#include "gemini.h"
#include "server.h"

enum gemini_result {
	GEMINI_OK,
	GEMINI_ERR_OOM,
	GEMINI_ERR_INVALID_URL,
	GEMINI_ERR_NOT_GEMINI,
	GEMINI_ERR_RESOLVE,
	GEMINI_ERR_CONNECT,
	GEMINI_ERR_SSL,
	GEMINI_ERR_SSL_VERIFY,
	GEMINI_ERR_IO,
	GEMINI_ERR_PROTOCOL,
	GEMINI_ERR_PROXY,
};

/* enum gemini_status { */
/* 	GEMINI_STATUS_INPUT = 10, */
/* 	GEMINI_STATUS_SENSITIVE_INPUT = 11, */
/* 	GEMINI_STATUS_SUCCESS = 20, */
/* 	GEMINI_STATUS_REDIRECT_TEMPORARY = 30, */
/* 	GEMINI_STATUS_REDIRECT_PERMANENT = 31, */
/* 	GEMINI_STATUS_TEMPORARY_FAILURE = 40, */
/* 	GEMINI_STATUS_SERVER_UNAVAILABLE = 41, */
/* 	GEMINI_STATUS_CGI_ERROR = 42, */
/* 	GEMINI_STATUS_PROXY_ERROR = 43, */
/* 	GEMINI_STATUS_SLOW_DOWN = 44, */
/* 	GEMINI_STATUS_PERMANENT_FAILURE = 50, */
/* 	GEMINI_STATUS_NOT_FOUND = 51, */
/* 	GEMINI_STATUS_GONE = 52, */
/* 	GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, */
/* 	GEMINI_STATUS_BAD_REQUEST = 59, */
/* 	GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, */
/* 	GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, */
/* 	GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, */
/* }; */

enum gemini_status_class {
	GEMINI_STATUS_CLASS_INPUT = 10,
	GEMINI_STATUS_CLASS_SUCCESS = 20,
	GEMINI_STATUS_CLASS_REDIRECT = 30,
	GEMINI_STATUS_CLASS_TEMPORARY_FAILURE = 40,
	GEMINI_STATUS_CLASS_PERMANENT_FAILURE = 50,
	GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED = 60,
};

struct gemini_response {
	enum gemini_status status;
	char *meta;

	// Response body may be read from here if appropriate:
	BIO *bio;

	// Connection state
	SSL_CTX *ssl_ctx;
	SSL *ssl;
	int fd;
};

struct gemini_options {
	// If NULL, an SSL context will be created. If unset, the ssl field
	// must also be NULL.
	SSL_CTX *ssl_ctx;

	// If ai_family != AF_UNSPEC (the default value on most systems), the
	// client will connect to this address and skip name resolution.
	struct addrinfo *addr;

	// If non-NULL, these hints are provided to getaddrinfo. Useful, for
	// example, to force IPv4/IPv6.
	struct addrinfo *hints;
};

// Requests the specified URL via the gemini protocol. If options is non-NULL,
// it may specify some additional configuration to adjust client behavior.
//
// Returns a value indicating the success of the request.
//
// Caller must call gemini_response_finish afterwards to clean up resources
// before exiting or re-using it for another request.
enum gemini_result gemini_request(
                struct gmnisrv_client *client,
//                const char *url,
                struct Curl_URL *,
//                char * proxy_host,
		struct gemini_options *options,
		struct gemini_response *resp);

// Must be called after gemini_request in order to free up the resources
// allocated during the request.
void gemini_response_finish(struct gemini_response *resp);

// Returns a user-friendly string describing an error.
const char *gemini_strerr(enum gemini_result r, struct gemini_response *resp);

// Returns the given URL with the input response set to the specified value.
// The caller must free the string.
char *gemini_input_url(const char *url, const char *input);

// Returns the general response class (i.e. with the second digit set to zero)
// of the given Gemini status code.
enum gemini_status_class gemini_response_class(enum gemini_status status);

int proxy_fetch (struct gmnisrv_client *client,
                 struct Curl_URL *u);

enum gemini_tok {
	GEMINI_TEXT,
	GEMINI_LINK,
	GEMINI_PREFORMATTED_BEGIN,
	GEMINI_PREFORMATTED_END,
	GEMINI_PREFORMATTED_TEXT,
	GEMINI_HEADING,
	GEMINI_LIST_ITEM,
	GEMINI_QUOTE,
};

struct gemini_token {
	enum gemini_tok token;

	// The token field determines which of the union members is valid.
	union {
		char *text;

		struct {
			char *text;
			char *url; // May be NULL
		} link;

		char *preformatted;

		struct {
			char *title;
			int level; // 1, 2, or 3
		} heading;

		char *list_item;
		char *quote_text;
	};
};

struct gemini_parser {
	BIO *f;
	char *buf;
	size_t bufsz;
	size_t bufln;
	bool preformatted;
};

// Initializes a text/gemini parser which reads from the specified BIO.
void gemini_parser_init(struct gemini_parser *p, BIO *f);

// Finishes this text/gemini parser and frees up its resources.
void gemini_parser_finish(struct gemini_parser *p);

// Reads the next token from a text/gemini file.
// 
// Returns 0 on success, 1 on EOF, and -1 on failure.
//
// Caller must call gemini_token_finish before exiting or re-using the token
// parameter.
int gemini_parser_next(struct gemini_parser *p, struct gemini_token *token);

// Must be called after gemini_next to free up resources for the next token.
void gemini_token_finish(struct gemini_token *token);

#endif
diff --git a/include/proxy/tofu.h b/include/proxy/tofu.h
new file mode 100644
index 0000000..a88167b
--- /dev/null
+++ b/include/proxy/tofu.h
@@ -0,0 +1,49 @@
#ifndef GEMINI_TOFU_H
#define GEMINI_TOFU_H
#include <limits.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <time.h>

enum tofu_error {
	TOFU_VALID,
	// Expired, wrong CN, etc.
	TOFU_INVALID_CERT,
	// Cert is valid but we haven't seen it before
	TOFU_UNTRUSTED_CERT,
	// Cert is valid but we already trust another cert for this host
	TOFU_FINGERPRINT_MISMATCH,
};

enum tofu_action {
	TOFU_ASK,
	TOFU_FAIL,
	TOFU_TRUST_ONCE,
	TOFU_TRUST_ALWAYS,
};

struct known_host {
	char *host, *fingerprint;
	time_t expires;
	int lineno;
	struct known_host *next;
};

// Called when the user needs to be prompted to agree to trust an unknown
// certificate. Return true to trust this certificate.
typedef enum tofu_action (tofu_callback_t)(enum tofu_error error,
	const char *fingerprint, struct known_host *host, void *data);

struct gemini_tofu {
	char known_hosts_path[PATH_MAX+1];
	struct known_host *known_hosts;
	int lineno;
	tofu_callback_t *callback;
	void *cb_data;
};

void gemini_tofu_init(struct gemini_tofu *tofu,
		SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *data);
void gemini_tofu_finish(struct gemini_tofu *tofu);

#endif
diff --git a/include/util.h b/include/util.h
index 8cbadb1..e30d186 100644
--- a/include/util.h
+++ b/include/util.h
@@ -1,6 +1,11 @@
#ifndef GEMINI_UTIL_H
#define GEMINI_UTIL_H

int mkdirs(char *path, mode_t mode);
struct pathspec {
	const char *var;
	const char *path;
};

int mkdirs(char *path, mode_t mode);
char *getpath(const struct pathspec *paths, size_t npaths);
#endif
diff --git a/src/config.c b/src/config.c
index c4c152c..5034e92 100644
--- a/src/config.c
+++ b/src/config.c
@@ -223,6 +223,7 @@ conf_ini_handler(void *user, const char *section,
		{ "rewrite", &route->rewrite },
		{ "root", &route->root },
		{ "index", &route->index },
		{ "proxy", &route->proxy },
	};
	struct {
		char *name;
diff --git a/src/proxy.c b/src/proxy.c
new file mode 100644
index 0000000..49393e9
--- /dev/null
+++ b/src/proxy.c
@@ -0,0 +1,584 @@
#include <assert.h>
#include <errno.h>
#include <getopt.h>
#include <netdb.h>
#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/ssl.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <termios.h>
#include <unistd.h>
#include "proxy/tofu.h"
#include "proxy/proxy.h"
#include "config.h"
#include "server.h"
#include "url.h"
#include "util.h"


static enum gemini_result
gemini_get_addrinfo(struct Curl_URL *uri, struct gemini_options *options, 
	struct gemini_response *resp, struct addrinfo **addr)
{
	int port = 1965;
	char *uport;
	if (curl_url_get(uri, CURLUPART_PORT, &uport, 0) == CURLUE_OK) {
		port = (int)strtol(uport, NULL, 10);
		free(uport);
	}

	if (options && options->addr && options->addr->ai_family != AF_UNSPEC) {
		*addr = options->addr;
	} else {
		struct addrinfo hints = {0};
		if (options && options->hints) {
			hints = *options->hints;
		} else {
			hints.ai_family = AF_UNSPEC;
		}
		hints.ai_socktype = SOCK_STREAM;

		char pbuf[7];
		snprintf(pbuf, sizeof(pbuf), "%d", port);

		char *domain;
		CURLUcode uc = curl_url_get(uri, CURLUPART_HOST, &domain, 0);
		assert(uc == CURLUE_OK);

		int r = getaddrinfo(domain, pbuf, &hints, addr);
		free(domain);
		if (r != 0) {
			resp->status = r;
			return GEMINI_ERR_RESOLVE;
		}
	}

	return GEMINI_OK;
}

static enum gemini_result
gemini_connect(struct Curl_URL *uri, struct gemini_options *options,
		struct gemini_response *resp, int *sfd)
{
	struct addrinfo *addr;
	enum gemini_result res = gemini_get_addrinfo(uri, options, resp, &addr);

	if (res != GEMINI_OK) {

		return res;
	}

	struct addrinfo *rp;
	for (rp = addr; rp != NULL; rp = rp->ai_next) {
		*sfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
                
		if (*sfd == -1) {
			continue;
		}
		if (connect(*sfd, rp->ai_addr, rp->ai_addrlen) != -1) {
			break;
		}
		close(*sfd);
	}
	if (rp == NULL) {
		resp->status = errno;
		res = GEMINI_ERR_CONNECT;
		return res;
	}

	if (!options || !options->addr) {
		freeaddrinfo(addr);
	}
	return res;
}

#define GEMINI_META_MAXLEN 1024
#define GEMINI_STATUS_MAXLEN 2

void proxy_parse(char *proxy_host, char ** phost, char ** pport) {
/*  these functions do not work with wide or multibyte characters */
  char *host_tmp = NULL;
  char *port_tmp = NULL;
  const char * scheme = "gemini://";
  assert(proxy_host);
  // see if proxy_host starts with scheme "gemini://"
  if (!strncmp(proxy_host, scheme, strlen(scheme)))
    {host_tmp = proxy_host + strlen(scheme);} else {
    host_tmp = proxy_host;}
  if ( (port_tmp = strrchr (host_tmp, ':'))) {
        *port_tmp = '\0'; //set ':' to null terminator
        port_tmp++; //step forward to number string
      }
  //take care of any trailing slash
  char *tmp = strchr(port_tmp,'/');
  if (tmp) *tmp = '\0';


  /* allow for direct ``proxy=:1999'' configuration */
  if(host_tmp && strlen(host_tmp)) *phost = strdup(host_tmp);

  if(port_tmp)  *pport = strdup(port_tmp);
}

enum gemini_result
gemini_request(struct gmnisrv_client *client, 
               struct Curl_URL *u, 
               struct gemini_options *options,
		struct gemini_response *resp)
{
        char *url;
        char *proxy_host;
	struct gmnisrv_route *route = client->host->routes;
          while (! route->proxy)
            {route = route->next; 
             break;
            }
        if (route->proxy){
          proxy_host = route->proxy; }
        else
          {        
           fprintf(stderr, "Problem with route->proxy");
           return GEMINI_ERR_PROXY;
          }
        //const char *proxy_host = "gemini://localhost:1966";
        url = proxy_host;
        char *phost = NULL, *pport = NULL;
        proxy_parse(proxy_host, &phost, &pport);

	assert(resp);
	struct Curl_URL *uri = curl_url_dup(u);


        if (phost) curl_url_set(uri, CURLUPART_HOST, phost, 0); 
        if (pport) curl_url_set(uri, CURLUPART_PORT, pport, 0); 

        free (phost);
        free (pport);

	resp->meta = NULL;
	memset(resp, 0, sizeof(*resp));
//	if (strlen(url) > 1024) {
	if (strlen(proxy_host) > 1024) {
		return GEMINI_ERR_INVALID_URL;
	}

	if (!uri) {
		return GEMINI_ERR_OOM;
	}

	enum gemini_result res = GEMINI_OK;

	char *scheme, *host;
	if (curl_url_get(uri, CURLUPART_SCHEME, &scheme, 0) != CURLUE_OK) {
		res = GEMINI_ERR_INVALID_URL;
		goto cleanup;
	} else {
		if (strcmp(scheme, "gemini") != 0) {
			res = GEMINI_ERR_NOT_GEMINI;
			free(scheme);
			goto cleanup;
		}
                free(scheme);
	}
	if (curl_url_get(uri, CURLUPART_HOST, &host, 0) != CURLUE_OK) {
		res = GEMINI_ERR_INVALID_URL;
		free(host);
		goto cleanup;
	}

        if (curl_url_get(uri, CURLUPART_URL, &url, 0) != CURLUE_OK) {
		res = GEMINI_ERR_INVALID_URL;
		free(url);
		goto cleanup;
	}

	if (options && options->ssl_ctx) {
		resp->ssl_ctx = options->ssl_ctx;
		SSL_CTX_up_ref(options->ssl_ctx);
	} else {
		resp->ssl_ctx = SSL_CTX_new(TLS_method());
		assert(resp->ssl_ctx);
		SSL_CTX_set_verify(resp->ssl_ctx, SSL_VERIFY_PEER, NULL);
	}

	int r;
	BIO *sbio = BIO_new(BIO_f_ssl());

	res = gemini_connect(uri, options, resp, &resp->fd);
	if (res != GEMINI_OK) {
		free(host);
		goto cleanup;
	}
	resp->ssl = SSL_new(resp->ssl_ctx);
	assert(resp->ssl);
	SSL_set_connect_state(resp->ssl);
	if ((r = SSL_set1_host(resp->ssl, host)) != 1) {
		free(host);
		goto ssl_error;
	}

	if ((r = SSL_set_tlsext_host_name(resp->ssl, host)) != 1) {
		free(host);
		goto ssl_error;
	}
	free(host);

	if ((r = SSL_set_fd(resp->ssl, resp->fd)) != 1) {
		goto ssl_error;
	}

	if ((r = SSL_connect(resp->ssl)) != 1) {
		goto ssl_error;
	}

	X509 *cert = SSL_get_peer_certificate(resp->ssl);

	if (!cert) {
		resp->status = X509_V_ERR_UNSPECIFIED;
		res = GEMINI_ERR_SSL_VERIFY;
		goto cleanup;
	}

	X509_free(cert);

	long vr = SSL_get_verify_result(resp->ssl);
	if (vr != X509_V_OK) {
		resp->status = vr;
		res = GEMINI_ERR_SSL_VERIFY;
		goto cleanup;
	}

	BIO_set_ssl(sbio, resp->ssl, 0);
	resp->bio = BIO_new(BIO_f_buffer());
	BIO_push(resp->bio, sbio);
	char req[1024 + 3];
	r = snprintf(req, sizeof(req), "%s\r\n", url);
	assert(r > 0);

	r = BIO_puts(sbio, req);
	if (r == -1) {
		res = GEMINI_ERR_IO;
		goto cleanup;
	}
	assert(r == (int)strlen(req));
	char buf[GEMINI_META_MAXLEN
		+ GEMINI_STATUS_MAXLEN
		+ 2 /* CRLF */ + 1 /* NUL */];

	r = BIO_gets(resp->bio, buf, sizeof(buf));
	if (r == -1) {
		res = GEMINI_ERR_IO;
		goto cleanup;
	}
	if (r < 3 || strcmp(&buf[r - 2], "\r\n") != 0) {
          fprintf(stderr, "RN HEADER FAILLURE\n");
		res = GEMINI_ERR_PROTOCOL;
		goto cleanup;
	}
	char *endptr;
	resp->status = (enum gemini_status)strtol(buf, &endptr, 10);
	if (*endptr != ' ' || resp->status < 10 || resp->status >= 70) {
		res = GEMINI_ERR_PROTOCOL;
                fprintf(stderr, "PROT STATUS FAILLURE\n");
		goto cleanup;
	}
	resp->meta = calloc(r - 5 /* 2 digits, space, and CRLF */ + 1 /* NUL */, 1);
	strncpy(resp->meta, &endptr[1], r - 5);
	resp->meta[r - 5] = '\0';

cleanup:
//        fprintf(stderr, "CLEANUP \n");
	curl_url_cleanup(uri);
	return res;
ssl_error:
        fprintf(stderr, "SSL ERROR!^_^\n");
	res = GEMINI_ERR_SSL;
	resp->status = r;
	goto cleanup;
}

void
gemini_response_finish(struct gemini_response *resp)
{
	if (!resp) {
		return;
	}

	if (resp->fd != -1) {
		close(resp->fd);
		resp->fd = -1;
	}

	if (resp->bio) {
		BIO_free(BIO_pop(resp->bio)); // ssl bio
		BIO_free(resp->bio); // buffered bio
		resp->bio = NULL;
	}

	SSL_free(resp->ssl);
	SSL_CTX_free(resp->ssl_ctx);
	free(resp->meta);

	resp->ssl = NULL;
	resp->ssl_ctx = NULL;
	resp->meta = NULL;
}

const char *
gemini_strerr(enum gemini_result r, struct gemini_response *resp)
{
	switch (r) {
	case GEMINI_OK:
		return "OK";
	case GEMINI_ERR_OOM:
		return "Out of memory";
	case GEMINI_ERR_INVALID_URL:
		return "Invalid URL";
	case GEMINI_ERR_NOT_GEMINI:
		return "Not a gemini URL";
	case GEMINI_ERR_RESOLVE:
		return gai_strerror(resp->status);
	case GEMINI_ERR_CONNECT:
		return strerror(errno);
	case GEMINI_ERR_SSL:
		return ERR_error_string(
			SSL_get_error(resp->ssl, resp->status),
			NULL);
        case GEMINI_ERR_SSL_VERIFY:
          return "SSL verify";
	case GEMINI_ERR_IO:
		return "I/O error";
	case GEMINI_ERR_PROTOCOL:
		return "Protocol error";
	case GEMINI_ERR_PROXY:
		return "Proxy problem";
	}
	assert(0);
}

char *
gemini_input_url(const char *url, const char *input)
{
	char *new_url = NULL;
	struct Curl_URL *uri = curl_url();
	if (!uri) {
		return NULL;
	}
	if (curl_url_set(uri, CURLUPART_URL, url, 0) != CURLUE_OK) {
		goto cleanup;
	}
	if (curl_url_set(uri, CURLUPART_QUERY, input, CURLU_URLENCODE) != CURLUE_OK) {
		goto cleanup;
	}
	if (curl_url_get(uri, CURLUPART_URL, &new_url, 0) != CURLUE_OK) {
		new_url = NULL;
		goto cleanup;
	}
cleanup:
	curl_url_cleanup(uri);
	return new_url;
}

enum gemini_status_class
gemini_response_class(enum gemini_status status)
{
	return status / 10 * 10;
}

int mkdirs(char *path, mode_t mode);
int download_resp(FILE *out, struct gemini_response resp, const char *path,
		char *url);


struct tofu_config {
	struct gemini_tofu tofu;
	enum tofu_action action;
};

static enum tofu_action
tofu_callback(enum tofu_error error, const char *fingerprint,
	struct known_host *host, void *data)
{
	struct tofu_config *cfg = (struct tofu_config *)data;
	enum tofu_action action = cfg->action;
	switch (error) {
	case TOFU_VALID:
		assert(0); // Invariant
	case TOFU_INVALID_CERT:
		fprintf(stderr,
			"The server presented an invalid certificate with fingerprint %s.\n",
			fingerprint);
		if (action == TOFU_TRUST_ALWAYS) {
			action = TOFU_TRUST_ONCE;
		}
		break;
	case TOFU_UNTRUSTED_CERT:
		fprintf(stderr,
			"The certificate offered by this server is of unknown trust. "
			"Its fingerprint is: \n"
			"%s\n\n"
			"Use '-j once' to trust temporarily, or '-j always' to add to the trust store.\n", fingerprint);
		break;
	case TOFU_FINGERPRINT_MISMATCH:
		fprintf(stderr,
			"The certificate offered by this server DOES NOT MATCH the one we have on file.\n"
			"/!\\ Someone may be eavesdropping on or manipulating this connection. /!\\\n"
			"The unknown certificate's fingerprint is:\n"
			"%s\n\n"
			"The expected fingerprint is:\n"
			"%s\n\n"
			"If you're certain that this is correct, edit %s:%d\n",
			fingerprint, host->fingerprint,
			cfg->tofu.known_hosts_path, host->lineno);
		return TOFU_FAIL;
	}

	if (action == TOFU_ASK) {
		return TOFU_FAIL;
	}

	return action;
}


/* end TOFU */

int
proxy_fetch (struct gmnisrv_client *client, struct Curl_URL *u)
{
	enum header_mode {
		OMIT_HEADERS,
		SHOW_HEADERS,
		ONLY_HEADERS,
	};
	enum header_mode header_mode = SHOW_HEADERS;

	/* enum input_mode { */
	/* 	INPUT_READ, */
	/* 	INPUT_SUPPRESS, */
	/* }; */

//	enum input_mode input_mode = INPUT_READ;

	bool follow_redirects = false, linefeed = true;
	int max_redirect = 5;

	struct addrinfo hints = {0};
	struct gemini_options opts = {
		.hints = &hints,
	};
	struct tofu_config cfg;

	cfg.action = TOFU_TRUST_ALWAYS;
        hints.ai_family = AF_INET;

	SSL_load_error_strings();
	ERR_load_crypto_strings();

	opts.ssl_ctx = SSL_CTX_new(TLS_method());
	gemini_tofu_init(&cfg.tofu, opts.ssl_ctx, &tofu_callback, &cfg);

	bool exit = false;

	int ret = 0, nredir = 0;
	while (!exit) {
		struct gemini_response resp;
		enum gemini_result r = gemini_request(client, u, &opts, &resp);
		if (r != GEMINI_OK) {
			fprintf(stderr, "Error: %s\n", gemini_strerr(r, &resp));
			ret = (int)r;
			exit = true;
			goto next;
		}

		switch (gemini_response_class(resp.status)) {
		case GEMINI_STATUS_CLASS_INPUT:
			/* if (input_mode == INPUT_SUPPRESS) { */
			/* 	exit = true; */
			/* 	break; */
			/* } */
                  fprintf(stderr, "Status Input\n");
                  exit = true;
                  break;
//			goto next;
		case GEMINI_STATUS_CLASS_REDIRECT:
			if (++nredir >= max_redirect) {
				fprintf(stderr,
					"Error: maximum redirects (%d) exceeded",
					max_redirect);
				exit = true;
				goto next;
			}

			if (!follow_redirects) {
				if (header_mode == OMIT_HEADERS) {
					fprintf(stderr, "REDIRECT: %d %s\n",
						resp.status, resp.meta);
				}
				exit = true;
			}
			goto next;
		case GEMINI_STATUS_CLASS_CLIENT_CERTIFICATE_REQUIRED:
			assert(0); // TODO
		case GEMINI_STATUS_CLASS_TEMPORARY_FAILURE:
		case GEMINI_STATUS_CLASS_PERMANENT_FAILURE:
			if (header_mode == OMIT_HEADERS) {
				fprintf(stderr, "%s: %d %s\n",
					resp.status / 10 == 4 ?
					"TEMPORARY FAILURE" : "PERMANENT FALIURE",
					resp.status, resp.meta);
			}
			exit = true;
			break;
		case GEMINI_STATUS_CLASS_SUCCESS:
			exit = true;
			break;
		}

                char head[GEMINI_META_MAXLEN];
                int size = snprintf(head, sizeof(head),"%d %s\r\n", resp.status, resp.meta);
//		printf("%d %s\r\n", resp.status, resp.meta);
                write(STDOUT_FILENO, head, size );
			char last;
			char buf[BUFSIZ];
			for (int n = 1; n > 0;) {
				n = BIO_read(resp.bio, buf, BUFSIZ);
//				n = BIO_gets(resp.bio, buf, BUFSIZ);
				if (n == -1) {
					fprintf(stderr, "Error: read\n");
					return 1;
				} else if (n != 0) {
					last = buf[n - 1];
				}
				ssize_t w = 0;
				while (w < (ssize_t)n) {
                                  // display the proxy return text
					ssize_t x = write(STDOUT_FILENO, &buf[w], n - w);
					if (x == -1) {
						fprintf(stderr, "Error: write: %s\n",
							strerror(errno));
						return 1;
					}
					w += x;
				}
			}
			if (strncmp(resp.meta, "text/", 5) == 0
					&& linefeed && last != '\n'
					&& isatty(STDOUT_FILENO)) {
				printf("\n");
			}
			break;
		

next:
		gemini_response_finish(&resp);
	} // while (!exit)

//	free(url);
	return ret;
}
diff --git a/src/serve.c b/src/serve.c
index e891d42..9bf829d 100644
--- a/src/serve.c
+++ b/src/serve.c
@@ -17,6 +17,7 @@
#include "mime.h"
#include "server.h"
#include "url.h"
#include "proxy/proxy.h"

void
client_submit_response(struct gmnisrv_client *client,
@@ -211,6 +212,64 @@ serve_cgi(struct gmnisrv_client *client, const char *path,
	}
}

static void
serve_proxy(struct gmnisrv_client *client)
{
	int pfd[2];
	if (pipe(pfd) == -1) {
		server_error("pipe: %s", strerror(errno));
		client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE,
			"Internal server error", NULL);
		return;
	}

	pid_t pid = fork();
	if (pid == -1) {
		server_error("fork: %s", strerror(errno));
		client_submit_response(client, GEMINI_STATUS_PERMANENT_FAILURE,
			"Internal server error", NULL);
		close(pfd[0]);
		close(pfd[1]);
		return;
	} else if (pid == 0) {
		close(pfd[0]);
		dup2(pfd[1], STDOUT_FILENO);
		close(pfd[1]);

		// Drew doesn't feel like freeing this stuff and this process is
		// going to die soon anyway so let's just be hip and call it an
		// arena allocator :^)
		struct Curl_URL *url = curl_url();
		assert(url);
		CURLUcode uc = curl_url_set(url, CURLUPART_URL, client->buf, 0);
		assert(uc == CURLUE_OK);

		char *query;
		uc = curl_url_get(url, CURLUPART_QUERY, &query, CURLU_URLDECODE);
		if (uc != CURLUE_OK) {
			assert(uc == CURLUE_NO_QUERY);
		} else {
			setenv("QUERY_STRING", query, 1);
		}

		char abuf[INET6_ADDRSTRLEN + 1];
		const char *addrs = inet_ntop(client->addr.sa_family,
			client->addr.sa_data, abuf, sizeof(abuf));
		assert(addrs);

                fprintf(stderr, "About to proxy_fetch\n");
                proxy_fetch(client,url);
		server_error("execlp: %s", strerror(errno));
		_exit(1);
	} else {
		close(pfd[1]);
		FILE *f = fdopen(pfd[0], "r");
		client_submit_response(client, GEMINI_STATUS_SUCCESS, "(proxy)", f);
		client->state = CLIENT_STATE_BODY; // The CGI script sends meta
		client->bufix = client->bufln = 0;
	}
}

static char *
ensure_buf(char *buf, size_t *sz, size_t desired)
{
@@ -389,17 +448,24 @@ serve_request(struct gmnisrv_client *client)
	while (route) {
		if (route_match(route, client_path, &url_path)) {
			break;
		}
		} else if (route->proxy) {
                  break;
                }
                route = route->next;
        }

		route = route->next;
	}

	if (!route) {
	if (!route && !route->proxy) {
		client_submit_response(client,
			GEMINI_STATUS_NOT_FOUND, "Not found", NULL);
		free(client_path);
	 	free(client_path); 
		free(url_path);
		return;
        }
	
        if (route->proxy) {
                    fprintf(stderr, "Route->proxy is handled\n");
                    serve_proxy(client);
		return;
	}

	assert(route->root); // TODO: reverse proxy support
@@ -421,6 +487,9 @@ serve_request(struct gmnisrv_client *client)
	int nlinks = 0;
	struct stat st;
	while (true) {
          if (route->proxy) { 
            break;
          }
		if ((n = stat(real_path, &st)) != 0) {
			if (route->cgi) {
				const char *new;
diff --git a/src/tofu.c b/src/tofu.c
new file mode 100644
index 0000000..5e3ec0b
--- /dev/null
+++ b/src/tofu.c
@@ -0,0 +1,232 @@
#include <assert.h>
#include <errno.h>
#include <libgen.h>
#include <limits.h>
#include <openssl/asn1.h>
#include <openssl/evp.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>
#include <openssl/x509v3.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "proxy/proxy.h"
#include "proxy/tofu.h"
#include "util.h"

static int
verify_callback(X509_STORE_CTX *ctx, void *data)
{
	// Gemini clients handle TLS verification differently from the rest of
	// the internet. We use a TOFU system, so trust is based on two factors:
	//
	// - Is the certificate valid at the time of the request?
	// - Has the user trusted this certificate yet?
	//
	// If the answer to the latter is "no", then we give the user an
	// opportunity to explicitly agree to trust the certificate before
	// rejecting it.
	//
	// If you're reading this code with the intent to re-use it for
	// something unrelated to Gemini, think twice.
	struct gemini_tofu *tofu = (struct gemini_tofu *)data;
	X509 *cert = X509_STORE_CTX_get0_cert(ctx);
	struct known_host *host = NULL;

	int rc;
	int day, sec;
	const ASN1_TIME *notBefore = X509_get0_notBefore(cert);
	const ASN1_TIME *notAfter = X509_get0_notAfter(cert);
	if (!ASN1_TIME_diff(&day, &sec, NULL, notBefore)) {
		rc = X509_V_ERR_UNSPECIFIED;
		goto invalid_cert;
	}
	if (day > 0 || sec > 0) {
		rc = X509_V_ERR_CERT_NOT_YET_VALID;
		goto invalid_cert;
	}
	if (!ASN1_TIME_diff(&day, &sec, NULL, notAfter)) {
		rc = X509_V_ERR_UNSPECIFIED;
		goto invalid_cert;
	}
	if (day < 0 || sec < 0) {
		rc = X509_V_ERR_CERT_HAS_EXPIRED;
		goto invalid_cert;
	}

	unsigned char md[512 / 8];
	const EVP_MD *sha512 = EVP_sha512();
	unsigned int len = sizeof(md);
	rc = X509_digest(cert, sha512, md, &len);
	assert(rc == 1);

	char fingerprint[512 / 8 * 3];
	for (size_t i = 0; i < sizeof(md); ++i) {
		snprintf(&fingerprint[i * 3], 4, "%02X%s",
			md[i], i + 1 == sizeof(md) ? "" : ":");
	}

	SSL *ssl = X509_STORE_CTX_get_ex_data(ctx,
		SSL_get_ex_data_X509_STORE_CTX_idx());
	const char *servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
	if (!servername) {
		rc = X509_V_ERR_HOSTNAME_MISMATCH;
		goto invalid_cert;
	}

	rc = X509_check_host(cert, servername, strlen(servername), 0, NULL);
	if (rc != 1) {
		rc = X509_V_ERR_HOSTNAME_MISMATCH;
		goto invalid_cert;
	}

	time_t now;
	time(&now);

	enum tofu_error error = TOFU_UNTRUSTED_CERT;
	host = tofu->known_hosts;
	while (host) {
		if (host->expires < now) {
			goto next;
		}
		if (strcmp(host->host, servername) != 0) {
			goto next;
		}
		if (strcmp(host->fingerprint, fingerprint) == 0) {
			// Valid match in known hosts
			return 0;
		}
		error = TOFU_FINGERPRINT_MISMATCH;
		break;
next:
		host = host->next;
	}

	rc = X509_V_ERR_CERT_UNTRUSTED;
	
callback:
	switch (tofu->callback(error, fingerprint, host, tofu->cb_data)) {
	case TOFU_ASK:
		assert(0); // Invariant
	case TOFU_FAIL:
		X509_STORE_CTX_set_error(ctx, rc);
		break;
	case TOFU_TRUST_ONCE:
		// No further action necessary
		return 0;
	case TOFU_TRUST_ALWAYS:;
		FILE *f = fopen(tofu->known_hosts_path, "a");
		if (!f) {
			fprintf(stderr, "Error opening %s for writing: %s\n",
				tofu->known_hosts_path, strerror(errno));
			break;
		};
		struct tm expires_tm;
		ASN1_TIME_to_tm(notAfter, &expires_tm);
		time_t expires = mktime(&expires_tm);
		fprintf(f, "%s %s %s %jd\n", servername,
			"SHA-512", fingerprint, (intmax_t)expires);
		fclose(f);

		host = calloc(1, sizeof(struct known_host));
		host->host = strdup(servername);
		host->fingerprint = strdup(fingerprint);
		host->expires = expires;
		host->lineno = ++tofu->lineno;
		host->next = tofu->known_hosts;
		tofu->known_hosts = host;
		return 0;
	}

	X509_STORE_CTX_set_error(ctx, rc);
	return 0;

invalid_cert:
	error = TOFU_INVALID_CERT;
	goto callback;
}


void
gemini_tofu_init(struct gemini_tofu *tofu,
	SSL_CTX *ssl_ctx, tofu_callback_t *cb, void *cb_data)
{
	const struct pathspec paths[] = {
		{.var = "GMNIDATA", .path = "/%s"},
		{.var = "XDG_DATA_HOME", .path = "/gemini/%s"},
		{.var = "HOME", .path = "/.local/share/gemini/%s"}
	};
	char *path_fmt = getpath(paths, sizeof(paths) / sizeof(paths[0]));
	char dname[PATH_MAX+1];
	size_t n = 0;

	n = snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path),
			path_fmt, "known_hosts");
	assert(n < sizeof(tofu->known_hosts_path));

	strncpy(dname, dirname(tofu->known_hosts_path), sizeof(dname)-1);
	if (mkdirs(dname, 0755) != 0) {
		snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path),
				path_fmt, "known_hosts");
		fprintf(stderr, "Error creating directory %s: %s\n",
				dirname(tofu->known_hosts_path), strerror(errno));
		return;
	}

	snprintf(tofu->known_hosts_path, sizeof(tofu->known_hosts_path),
			path_fmt, "known_hosts");
	free(path_fmt);

	tofu->callback = cb;
	tofu->cb_data = cb_data;
	SSL_CTX_set_cert_verify_callback(ssl_ctx, verify_callback, tofu);

	tofu->known_hosts = NULL;

	FILE *f = fopen(tofu->known_hosts_path, "r");
	if (!f) {
		return;
	}
	n = 0;
	char *line = NULL;
	while (getline(&line, &n, f) != -1) {
		struct known_host *host = calloc(1, sizeof(struct known_host));
		char *tok = strtok(line, " ");
		assert(tok);
		host->host = strdup(tok);

		tok = strtok(NULL, " ");
		assert(tok);
		if (strcmp(tok, "SHA-512") != 0) {
			free(host->host);
			free(host);
			continue;
		}

		tok = strtok(NULL, " ");
		assert(tok);
		host->fingerprint = strdup(tok);

		tok = strtok(NULL, " ");
		assert(tok);
		host->expires = strtoul(tok, NULL, 10);

		host->next = tofu->known_hosts;
		tofu->known_hosts = host;
	}
	free(line);
	fclose(f);
}

void
gemini_tofu_finish(struct gemini_tofu *tofu)
{
	struct known_host *host = tofu->known_hosts;
	while (host) {
		struct known_host *tmp = host;
		host = host->next;
		free(tmp->host);
		free(tmp->fingerprint);
		free(tmp);
	}
}
diff --git a/src/util.c b/src/util.c
index 8cc5502..7ae7076 100644
--- a/src/util.c
+++ b/src/util.c
@@ -44,3 +44,20 @@ mkdirs(char *path, mode_t mode)
	errno = 0;
	return 0;
}

char *
getpath(const struct pathspec *paths, size_t npaths) {
	for (size_t i = 0; i < npaths; i++) {
		const char *var = "";
		if (paths[i].var) {
			var = getenv(paths[i].var);
		}
		if (var) {
			char *out = calloc(1,
				strlen(var) + strlen(paths[i].path) + 1);
			strcat(strcat(out, var), paths[i].path);
			return out;
		}
	}
	return NULL;
}
-- 
2.26.2

[PATCH gmnisrv 2/2] removed sloppy comment Export this patch

From: Andreas R <avr@geminet.org>

---
 include/proxy/proxy.h | 21 ---------------------
 1 file changed, 21 deletions(-)

diff --git a/include/proxy/proxy.h b/include/proxy/proxy.h
index 66986df..cde8261 100644
--- a/include/proxy/proxy.h
+++ b/include/proxy/proxy.h
@@ -21,27 +21,6 @@ enum gemini_result {
	GEMINI_ERR_PROXY,
};

/* enum gemini_status { */
/* 	GEMINI_STATUS_INPUT = 10, */
/* 	GEMINI_STATUS_SENSITIVE_INPUT = 11, */
/* 	GEMINI_STATUS_SUCCESS = 20, */
/* 	GEMINI_STATUS_REDIRECT_TEMPORARY = 30, */
/* 	GEMINI_STATUS_REDIRECT_PERMANENT = 31, */
/* 	GEMINI_STATUS_TEMPORARY_FAILURE = 40, */
/* 	GEMINI_STATUS_SERVER_UNAVAILABLE = 41, */
/* 	GEMINI_STATUS_CGI_ERROR = 42, */
/* 	GEMINI_STATUS_PROXY_ERROR = 43, */
/* 	GEMINI_STATUS_SLOW_DOWN = 44, */
/* 	GEMINI_STATUS_PERMANENT_FAILURE = 50, */
/* 	GEMINI_STATUS_NOT_FOUND = 51, */
/* 	GEMINI_STATUS_GONE = 52, */
/* 	GEMINI_STATUS_PROXY_REQUEST_REFUSED = 53, */
/* 	GEMINI_STATUS_BAD_REQUEST = 59, */
/* 	GEMINI_STATUS_CLIENT_CERTIFICATE_REQUIRED = 60, */
/* 	GEMINI_STATUS_CERTIFICATE_NOT_AUTHORIZED = 61, */
/* 	GEMINI_STATUS_CERTIFICATE_NOT_VALID = 62, */
/* }; */

enum gemini_status_class {
	GEMINI_STATUS_CLASS_INPUT = 10,
	GEMINI_STATUS_CLASS_SUCCESS = 20,
-- 
2.26.2