diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | ex.c | 58 | ||||
-rw-r--r-- | gmid.c | 2 | ||||
-rw-r--r-- | gmid.h | 26 | ||||
-rw-r--r-- | parse.y | 24 | ||||
-rw-r--r-- | proxy.c | 318 | ||||
-rw-r--r-- | server.c | 112 |
7 files changed, 538 insertions, 4 deletions
@@ -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} @@ -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, }; @@ -425,6 +427,62 @@ handle_imsg_fcgi_req(struct imsgbuf *ibuf, struct imsg *imsg, size_t datalen) } 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) { int i; @@ -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); @@ -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**); @@ -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}, @@ -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; +} @@ -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); } @@ -1380,6 +1468,30 @@ handle_imsg_fcgi_fd(struct imsgbuf *ibuf, struct imsg *imsg, size_t len) } 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) { /* |