add ability to proxy requests

Add to gmid the ability to forwad a request to another gemini server and
thus acting like a reverse proxy.  The current syntax for the config
file is

	server "example.com" {
		...
		proxy relay-to host:port
	}

Further options (like the use of custom certificates) are planned.

cf. github issue #7
This commit is contained in:
Omar Polo 2021-12-29 20:36:54 +00:00
parent 054387bb26
commit 72b033ef18
7 changed files with 538 additions and 4 deletions

View File

@ -15,7 +15,7 @@ y.tab.c: parse.y
${YACC} -b y parse.y
SRCS = gmid.c iri.c utf8.c ex.c server.c sandbox.c mime.c puny.c \
utils.c log.c dirs.c fcgi.c
utils.c log.c dirs.c fcgi.c proxy.c
OBJS = ${SRCS:.c=.o} y.tab.o ${COMPAT}
gmid: ${OBJS}

58
ex.c
View File

@ -31,12 +31,14 @@
static void handle_imsg_cgi_req(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_fcgi_req(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_conn_req(struct imsgbuf *, struct imsg *, size_t);
static void handle_imsg_quit(struct imsgbuf*, struct imsg*, size_t);
static void handle_dispatch_imsg(int, short, void*);
static imsg_handlerfn *handlers[] = {
[IMSG_FCGI_REQ] = handle_imsg_fcgi_req,
[IMSG_CGI_REQ] = handle_imsg_cgi_req,
[IMSG_CONN_REQ] = handle_imsg_conn_req,
[IMSG_QUIT] = handle_imsg_quit,
};
@ -424,6 +426,62 @@ handle_imsg_fcgi_req(struct imsgbuf *ibuf, struct imsg *imsg, size_t datalen)
imsg_flush(ibuf);
}
static void
handle_imsg_conn_req(struct imsgbuf *ibuf, struct imsg *imsg, size_t datalen)
{
struct addrinfo hints, *res, *res0;
struct connreq req;
int r, sock;
if (datalen != sizeof(req))
abort();
memcpy(&req, imsg->data, sizeof(req));
req.flag = 0;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
/* XXX: do this asynchronously if possible */
r = getaddrinfo(req.host, req.port, &hints, &res0);
if (r != 0) {
log_warn(NULL, "getaddrinfo(\"%s\", \"%s\"): %s",
req.host, req.port, gai_strerror(r));
goto err;
}
for (res = res0; res; res = res->ai_next) {
sock = socket(res->ai_family, res->ai_socktype,
res->ai_protocol);
if (sock == -1)
continue;
if (connect(sock, res->ai_addr, res->ai_addrlen) == -1) {
close(sock);
sock = -1;
continue;
}
break;
}
freeaddrinfo(res0);
if (sock == -1) {
log_warn(NULL, "can't connect to %s:%s", req.host,
req.port);
goto err;
}
imsg_compose(ibuf, IMSG_CONN_FD, imsg->hdr.peerid, 0, sock, NULL, 0);
imsg_flush(ibuf);
return;
err:
imsg_compose(ibuf, IMSG_CONN_FD, imsg->hdr.peerid, 0, -1, NULL, 0);
imsg_flush(ibuf);
}
static void
handle_imsg_quit(struct imsgbuf *ibuf, struct imsg *imsg, size_t datalen)
{

2
gmid.c
View File

@ -302,6 +302,8 @@ free_config(void)
free((char*)l->block_fmt);
free((char*)l->dir);
free(l->proxy_host);
if (l->dirfd != -1)
close(l->dirfd);

26
gmid.h
View File

@ -59,6 +59,7 @@
#define TEMP_REDIRECT 30
#define TEMP_FAILURE 40
#define CGI_ERROR 42
#define PROXY_ERROR 43
#define NOT_FOUND 51
#define PROXY_REFUSED 53
#define BAD_REQUEST 59
@ -110,6 +111,9 @@ struct location {
int disable_log;
int fcgi;
char *proxy_host;
const char *proxy_port;
const char *dir;
int dirfd;
@ -193,10 +197,14 @@ enum {
REQUEST_DIR,
REQUEST_CGI,
REQUEST_FCGI,
REQUEST_PROXY,
REQUEST_DONE,
};
#define IS_INTERNAL_REQUEST(x) ((x) != REQUEST_CGI && (x) != REQUEST_FCGI)
#define IS_INTERNAL_REQUEST(x) \
((x) != REQUEST_CGI && \
(x) != REQUEST_FCGI && \
(x) != REQUEST_PROXY)
struct client {
uint32_t id;
@ -211,6 +219,10 @@ struct client {
struct bufferevent *cgibev;
struct bufferevent *proxybev;
struct tls *proxyctx;
struct event proxyev;
char *header;
int code;
@ -262,6 +274,12 @@ struct cgireq {
size_t loc_off;
};
struct connreq {
char host[NI_MAXHOST];
char port[NI_MAXSERV];
int flag;
};
enum {
FILE_EXISTS,
FILE_EXECUTABLE,
@ -274,6 +292,8 @@ enum imsg_type {
IMSG_CGI_RES,
IMSG_FCGI_REQ,
IMSG_FCGI_FD,
IMSG_CONN_REQ,
IMSG_CONN_FD,
IMSG_LOG,
IMSG_LOG_REQUEST,
IMSG_LOG_TYPE,
@ -322,6 +342,7 @@ const char *vhost_default_mime(struct vhost*, const char*);
const char *vhost_index(struct vhost*, const char*);
int vhost_auto_index(struct vhost*, const char*);
int vhost_block_return(struct vhost*, const char*, int*, const char**);
struct location *vhost_reverse_proxy(struct vhost *, const char *);
int vhost_fastcgi(struct vhost*, const char*);
int vhost_dirfd(struct vhost*, const char*, size_t*);
int vhost_strip(struct vhost*, const char*);
@ -378,6 +399,9 @@ int parse_iri(char*, struct iri*, const char**);
int serialize_iri(struct iri*, char*, size_t);
char *pct_decode_str(char *);
/* proxy.c */
int proxy_init(struct client *);
/* puny.c */
int puny_decode(const char*, char*, size_t, const char**);

24
parse.y
View File

@ -121,8 +121,8 @@ typedef struct {
%token LANG LOCATION LOG
%token MAP MIME
%token OCSP OFF ON
%token PARAM PORT PREFORK PROTOCOLS
%token REQUIRE RETURN ROOT
%token PARAM PORT PREFORK PROTOCOLS PROXY
%token RELAY_TO REQUIRE RETURN ROOT
%token SERVER SPAWN STRIP
%token TCP TOEXT TYPE USER
@ -330,6 +330,24 @@ locopt : AUTO INDEX bool { loc->auto_index = $3 ? 1 : -1; }
loc->lang = $2;
}
| LOG bool { loc->disable_log = !$2; }
| PROXY RELAY_TO string {
char *at;
const char *errstr;
only_once(loc->proxy_host, "proxy relay-to");
loc->proxy_host = $3;
if ((at = strchr($3, ':')) != NULL) {
*at++ = '\0';
loc->proxy_port = at;
} else
loc->proxy_port = "1965";
strtonum(loc->proxy_port, 1, UINT16_MAX, &errstr);
if (errstr != NULL)
yyerror("proxy port is %s: %s", errstr,
loc->proxy_port);
}
| REQUIRE CLIENT CA string {
only_once(loc->reqca, "require client ca");
ensure_absolute_path($4);
@ -408,6 +426,8 @@ static struct keyword {
{"port", PORT},
{"prefork", PREFORK},
{"protocols", PROTOCOLS},
{"proxy", PROXY},
{"relay-to", RELAY_TO},
{"require", REQUIRE},
{"return", RETURN},
{"root", ROOT},

318
proxy.c Normal file
View File

@ -0,0 +1,318 @@
/*
* Copyright (c) 2021 Omar Polo <op@omarpolo.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "gmid.h"
#include <ctype.h>
#include <errno.h>
#include <string.h>
#define MIN(a, b) ((a) < (b) ? (a) : (b))
static struct timeval handshake_timeout = { 5, 0 };
static void proxy_tls_readcb(int, short, void *);
static void proxy_tls_writecb(int, short, void *);
static void proxy_read(struct bufferevent *, void *);
static void proxy_write(struct bufferevent *, void *);
static void proxy_error(struct bufferevent *, short, void *);
static void
proxy_tls_readcb(int fd, short event, void *d)
{
struct bufferevent *bufev = d;
struct client *c = bufev->cbarg;
char buf[IBUF_READ_SIZE];
int what = EVBUFFER_READ;
int howmuch = IBUF_READ_SIZE;
int res;
ssize_t ret;
size_t len;
if (event == EV_TIMEOUT) {
what |= EVBUFFER_TIMEOUT;
goto err;
}
if (bufev->wm_read.high != 0)
howmuch = MIN(sizeof(buf), bufev->wm_read.high);
switch (ret = tls_read(c->proxyctx, buf, howmuch)) {
case TLS_WANT_POLLIN:
case TLS_WANT_POLLOUT:
goto retry;
case -1:
what |= EVBUFFER_ERROR;
goto err;
}
len = ret;
if (len == 0) {
what |= EVBUFFER_EOF;
goto err;
}
res = evbuffer_add(bufev->input, buf, len);
if (res == -1) {
what |= EVBUFFER_ERROR;
goto err;
}
event_add(&bufev->ev_read, NULL);
len = EVBUFFER_LENGTH(bufev->input);
if (bufev->wm_read.low != 0 && len < bufev->wm_read.low)
return;
if (bufev->readcb != NULL)
(*bufev->readcb)(bufev, bufev->cbarg);
return;
retry:
event_add(&bufev->ev_read, NULL);
return;
err:
(*bufev->errorcb)(bufev, what, bufev->cbarg);
}
static void
proxy_tls_writecb(int fd, short event, void *d)
{
struct bufferevent *bufev = d;
struct client *c = bufev->cbarg;
ssize_t ret;
size_t len;
short what = EVBUFFER_WRITE;
if (event & EV_TIMEOUT) {
what |= EVBUFFER_TIMEOUT;
goto err;
}
if (EVBUFFER_LENGTH(bufev->output) != 0) {
ret = tls_write(c->proxyctx, EVBUFFER_DATA(bufev->output),
EVBUFFER_LENGTH(bufev->output));
switch (ret) {
case TLS_WANT_POLLIN:
case TLS_WANT_POLLOUT:
goto retry;
case -1:
what |= EVBUFFER_ERROR;
goto err;
}
len = ret;
evbuffer_drain(bufev->output, len);
}
if (EVBUFFER_LENGTH(bufev->output) != 0)
event_add(&bufev->ev_write, NULL);
if (bufev->writecb != NULL &&
EVBUFFER_LENGTH(bufev->output) <= bufev->wm_write.low)
(*bufev->writecb)(bufev, bufev->cbarg);
return;
retry:
event_add(&bufev->ev_write, NULL);
return;
err:
(*bufev->errorcb)(bufev, what, bufev->cbarg);
}
static void
proxy_read(struct bufferevent *bev, void *d)
{
struct client *c = d;
struct evbuffer *src = EVBUFFER_INPUT(bev);
char *hdr;
size_t len;
int code;
/* intercept the header */
if (c->code == 0) {
hdr = evbuffer_readln(src, &len, EVBUFFER_EOL_CRLF_STRICT);
if (hdr == NULL) {
/* max reply + \r\n */
if (EVBUFFER_LENGTH(src) > 1029) {
log_warn(c, "upstream server is trying to "
"send a header that's too long.");
proxy_error(bev, EVBUFFER_READ, c);
}
/* wait a bit */
return;
}
if (len < 3 || len > 1029 ||
!isdigit(hdr[0]) ||
!isdigit(hdr[1]) ||
!isspace(hdr[2])) {
free(hdr);
log_warn(c, "upstream server is trying to send a "
"header that's too long.");
proxy_error(bev, EVBUFFER_READ, c);
return;
}
c->header = hdr;
code = (hdr[0] - '0') * 10 + (hdr[1] - '0');
if (code < 10 || code >= 70) {
log_warn(c, "upstream server is trying to send an "
"invalid reply code: %d", code);
proxy_error(bev, EVBUFFER_READ, c);
return;
}
start_reply(c, code, hdr + 3);
if (c->code < 20 || c->code > 29) {
proxy_error(bev, EVBUFFER_EOF, c);
return;
}
}
bufferevent_write_buffer(c->bev, src);
}
static void
proxy_write(struct bufferevent *bev, void *d)
{
struct evbuffer *dst = EVBUFFER_OUTPUT(bev);
/* request successfully sent */
if (EVBUFFER_LENGTH(dst) == 0)
bufferevent_disable(bev, EV_WRITE);
}
static void
proxy_error(struct bufferevent *bev, short error, void *d)
{
struct client *c = d;
/*
* If we're here it means that some kind of non-recoverable
* error appened.
*/
bufferevent_free(bev);
c->proxybev = NULL;
tls_free(c->proxyctx);
c->proxyctx = NULL;
close(c->pfd);
c->pfd = -1;
/* EOF and no header */
if (c->code == 0) {
start_reply(c, PROXY_ERROR, "protocol error");
return;
}
c->type = REQUEST_DONE;
client_write(c->bev, c);
}
static void
proxy_handshake(int fd, short event, void *d)
{
struct client *c = d;
struct evbuffer *evb;
char iribuf[GEMINI_URL_LEN];
if (event == EV_TIMEOUT) {
start_reply(c, PROXY_ERROR, "timeout");
return;
}
switch (tls_handshake(c->proxyctx)) {
case TLS_WANT_POLLIN:
event_set(&c->proxyev, fd, EV_READ, proxy_handshake, c);
event_add(&c->proxyev, &handshake_timeout);
return;
case TLS_WANT_POLLOUT:
event_set(&c->proxyev, fd, EV_WRITE, proxy_handshake, c);
event_add(&c->proxyev, &handshake_timeout);
return;
case -1:
log_warn(c, "handshake with proxy failed: %s",
tls_error(c->proxyctx));
start_reply(c, PROXY_ERROR, "handshake failed");
return;
}
c->proxybev = bufferevent_new(c->pfd, proxy_read, proxy_write,
proxy_error, c);
if (c->proxybev == NULL)
fatal("can't allocate bufferevent: %s", strerror(errno));
event_set(&c->proxybev->ev_read, c->pfd, EV_READ,
proxy_tls_readcb, c->proxybev);
event_set(&c->proxybev->ev_write, c->pfd, EV_WRITE,
proxy_tls_writecb, c->proxybev);
#if HAVE_LIBEVENT2
evbuffer_unfreeze(c->proxybev->input, 0);
evbuffer_unfreeze(c->proxybev->output, 1);
#endif
serialize_iri(&c->iri, iribuf, sizeof(iribuf));
evb = EVBUFFER_OUTPUT(c->proxybev);
evbuffer_add_printf(evb, "%s\r\n", iribuf);
bufferevent_enable(c->proxybev, EV_READ|EV_WRITE);
}
int
proxy_init(struct client *c)
{
struct tls_config *conf = NULL;
c->type = REQUEST_PROXY;
if ((conf = tls_config_new()) == NULL)
return -1;
/* TODO: tls_config_set_protocols here */
/* TODO: optionally load a client keypair here */
tls_config_insecure_noverifycert(conf);
if ((c->proxyctx = tls_client()) == NULL)
goto err;
if (tls_configure(c->proxyctx, conf) == -1)
goto err;
if (tls_connect_socket(c->proxyctx, c->pfd, c->domain) == -1)
goto err;
event_set(&c->proxyev, c->pfd, EV_READ|EV_WRITE, proxy_handshake, c);
event_add(&c->proxyev, &handshake_timeout);
tls_config_free(conf);
return 0;
err:
tls_config_free(conf);
if (c->proxyctx != NULL)
tls_free(c->proxyctx);
return -1;
}

112
server.c
View File

@ -47,6 +47,7 @@ static void handle_handshake(int, short, void*);
static const char *strip_path(const char*, int);
static void fmt_sbuf(const char*, struct client*, const char*);
static int apply_block_return(struct client*);
static int apply_reverse_proxy(struct client *);
static int apply_fastcgi(struct client*);
static int apply_require_ca(struct client*);
static size_t host_nth(struct vhost*);
@ -72,6 +73,7 @@ static struct client *client_by_id(int);
static void handle_imsg_cgi_res(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_fcgi_fd(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_conn_fd(struct imsgbuf*, struct imsg*, size_t);
static void handle_imsg_quit(struct imsgbuf*, struct imsg*, size_t);
static void handle_dispatch_imsg(int, short, void *);
static void handle_siginfo(int, short, void*);
@ -80,6 +82,7 @@ static imsg_handlerfn *handlers[] = {
[IMSG_QUIT] = handle_imsg_quit,
[IMSG_CGI_RES] = handle_imsg_cgi_res,
[IMSG_FCGI_FD] = handle_imsg_fcgi_fd,
[IMSG_CONN_FD] = handle_imsg_conn_fd,
};
static uint32_t server_client_id;
@ -204,6 +207,27 @@ vhost_block_return(struct vhost *v, const char *path, int *code, const char **fm
return loc->block_code != 0;
}
struct location *
vhost_reverse_proxy(struct vhost *v, const char *path)
{
struct location *loc;
if (v == NULL || path == NULL)
return NULL;
loc = TAILQ_FIRST(&v->locations);
while ((loc = TAILQ_NEXT(loc, locations)) != NULL) {
if (loc->proxy_host != NULL)
if (matches(loc->match, path))
return loc;
}
loc = TAILQ_FIRST(&v->locations);
if (loc->proxy_host != NULL)
return loc;
return NULL;
}
int
vhost_fastcgi(struct vhost *v, const char *path)
{
@ -602,6 +626,30 @@ apply_block_return(struct client *c)
return 1;
}
/* 1 if matching a proxy relay-to (and apply it), 0 otherwise */
static int
apply_reverse_proxy(struct client *c)
{
struct location *loc;
struct connreq r;
if ((loc = vhost_reverse_proxy(c->host, c->iri.path)) == NULL)
return 0;
log_debug(c, "opening proxy connection for %s:%s",
loc->proxy_host, loc->proxy_port);
strlcpy(r.host, loc->proxy_host, sizeof(r.host));
strlcpy(r.port, loc->proxy_port, sizeof(r.port));
strlcpy(c->domain, loc->proxy_host, sizeof(c->domain));
imsg_compose(&exibuf, IMSG_CONN_REQ, c->id, 0, -1, &r, sizeof(r));
imsg_flush(&exibuf);
return 1;
}
/* 1 if matching `fcgi' (and apply it), 0 otherwise */
static int
apply_fastcgi(struct client *c)
@ -963,6 +1011,9 @@ client_read(struct bufferevent *bev, void *d)
return;
}
if (apply_reverse_proxy(c))
return;
/* ignore the port number */
if (strcmp(c->iri.schema, "gemini") ||
strcmp(decoded, c->domain)) {
@ -1030,6 +1081,7 @@ client_write(struct bufferevent *bev, void *d)
case REQUEST_CGI:
case REQUEST_FCGI:
case REQUEST_PROXY:
/*
* Here we depend on on the cgi script or fastcgi
* connection to provide data.
@ -1091,6 +1143,7 @@ start_reply(struct client *c, int code, const char *meta)
if (c->type != REQUEST_CGI &&
c->type != REQUEST_FCGI &&
c->type != REQUEST_PROXY &&
!strcmp(meta, "text/gemini") &&
(lang = vhost_lang(c->host, c->iri.path)) != NULL) {
rr = evbuffer_add_printf(evb, ";lang=%s", lang);
@ -1155,6 +1208,29 @@ client_close_ev(int fd, short event, void *d)
c->fd = -1;
}
static void
client_proxy_close(int fd, short event, void *d)
{
struct tls *ctx = d;
if (ctx == NULL) {
close(fd);
return;
}
switch (tls_close(ctx)) {
case TLS_WANT_POLLIN:
event_once(fd, EV_READ, client_proxy_close, d, NULL);
break;
case TLS_WANT_POLLOUT:
event_once(fd, EV_WRITE, client_proxy_close, d, NULL);
break;
}
tls_free(ctx);
close(fd);
}
void
client_close(struct client *c)
{
@ -1179,6 +1255,18 @@ client_close(struct client *c)
bufferevent_free(c->bev);
c->bev = NULL;
if (c->proxybev != NULL) {
if (event_pending(&c->proxyev, EV_READ|EV_WRITE, NULL))
event_del(&c->proxyev);
if (c->pfd != -1) {
client_proxy_close(c->pfd, 0, c->proxyctx);
c->pfd = -1;
}
bufferevent_free(c->proxybev);
}
client_close_ev(c->fd, 0, c);
}
@ -1379,6 +1467,30 @@ handle_imsg_fcgi_fd(struct imsgbuf *ibuf, struct imsg *imsg, size_t len)
fcgi_req(c);
}
static void
handle_imsg_conn_fd(struct imsgbuf *ibuf, struct imsg *imsg, size_t len)
{
struct client *c;
int id;
id = imsg->hdr.peerid;
if ((c = try_client_by_id(id)) == NULL) {
if (imsg->fd != -1)
close(imsg->fd);
return;
}
if ((c->pfd = imsg->fd) == -1) {
start_reply(c, PROXY_ERROR, "proxy error");
return;
}
mark_nonblock(c->pfd);
if (proxy_init(c) == -1)
start_reply(c, PROXY_ERROR, "proxy error");
}
static void
handle_imsg_quit(struct imsgbuf *ibuf, struct imsg *imsg, size_t len)
{