~sircmpwn/gmni-discuss

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
3 2

[PATCH gmnisrv 0/2] gmnisrv reverse proxy

~avr
Details
Message ID
<161227026645.16482.2466538986550286273-0@git.sr.ht>
DKIM signature
missing
Download raw message
I needed reverse proxy functionality for gemini, and this patch set
implements that.

It's a hack&slash job on Drew's 'gmni' client, then
integrated into gmnisrv. The damage is mostly contained to src/proxy.c
and include/proxy/

I found a hint of reverse proxying already
existing in the source, so it might be worth it to wait for that, but if
you need reverse proxy funcitonality *now*, this patch might do the job.

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

 configure             |   4 +-
 doc/gmnisrvini.scd    |   6 +
 include/config.h      |   1 +
 include/proxy/proxy.h | 153 +++++++++++
 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, 1127 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

-- 
2.26.2

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

~avr
Details
Message ID
<161227026645.16482.2466538986550286273-1@git.sr.ht>
In-Reply-To
<161227026645.16482.2466538986550286273-0@git.sr.ht> (view parent)
DKIM signature
missing
Download raw message
Patch: +1148 -8
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

~avr
Details
Message ID
<161227026645.16482.2466538986550286273-2@git.sr.ht>
In-Reply-To
<161227026645.16482.2466538986550286273-0@git.sr.ht> (view parent)
DKIM signature
missing
Download raw message
Patch: +0 -21
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
Details
Message ID
<C8Z5881RP9B7.3CPB8EEBFS0Y1@taiga>
In-Reply-To
<161227026645.16482.2466538986550286273-0@git.sr.ht> (view parent)
DKIM signature
fail
Download raw message
DKIM signature: fail
This is a bit of a mess. First, it should be implemented by linking to
libgmni, not by vendoring the code. Second, the code needs to conform to
our style requirements. Use tabs, not spaces, don't leave code commented
out, etc.
Reply to thread Export thread (mbox)