Commit Diff


commit - 054387bb26e75cef12e8dc0f531e7ee42614edd7
commit + 72b033ef18ae3f82922f6f11ce0f5194e95f667d
blob - fd02c845f22019234d4c77477fc88bd89b3c3fb6
blob + bf7836ced797115f8b13bf598d2e8fe1ad4fc22a
--- Makefile
+++ Makefile
@@ -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}
blob - ad21cd5f4fdd62b6153fbe4dd7bd7471e03b8b61
blob + 425c7fe3946e7f31b7a79eac350207390fde3cb9
--- ex.c
+++ ex.c
@@ -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,
 };
 
@@ -421,7 +423,63 @@ handle_imsg_fcgi_req(struct imsgbuf *ibuf, struct imsg
 		fd = fcgi_open_sock(f);
 
 	imsg_compose(ibuf, IMSG_FCGI_FD, imsg->hdr.peerid, 0, fd, NULL, 0);
+	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
blob - e0c777afa714a54342e2536da4b86ecca9280b44
blob + 7c2c7bd55cf93fa423b2c8e3a3144fd402eddfd0
--- gmid.c
+++ gmid.c
@@ -301,6 +301,8 @@ free_config(void)
 			free((char*)l->index);
 			free((char*)l->block_fmt);
 			free((char*)l->dir);
+
+			free(l->proxy_host);
 
 			if (l->dirfd != -1)
 				close(l->dirfd);
blob - a92665c03ce0ddf9d31467e48f7b5ba8b7924ec5
blob + 6798800929649345dc3aea95169a0a0aa01a4710
--- gmid.h
+++ gmid.h
@@ -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;
@@ -210,6 +218,10 @@ struct client {
 	int		 type;
 
 	struct bufferevent *cgibev;
+
+	struct bufferevent *proxybev;
+	struct tls	*proxyctx;
+	struct event	 proxyev;
 
 	char		*header;
 
@@ -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 ch
 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**);
 
blob - 8a9bae012f1bfe907bca072dddb481f3b623889f
blob + bab52091a7176665180a57639ab7826c9bb3e6ad
--- parse.y
+++ parse.y
@@ -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 :
 			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},
blob - /dev/null
blob + 72ce9f7b14d8c082b0354ade2730b96b46c3f480 (mode 644)
--- /dev/null
+++ proxy.c
@@ -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;
+}
blob - 151ede75cf8dbc536eff07dea3698e36e5b93344
blob + 402eda4b5c529bfb1d2714dd85bc78aa1da9aa8a
--- server.c
+++ server.c
@@ -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;
@@ -202,6 +205,27 @@ vhost_block_return(struct vhost *v, const char *path, 
 	*code = loc->block_code;
 	*fmt = loc->block_fmt;
 	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
@@ -599,6 +623,30 @@ apply_block_return(struct client *c)
 	fmt_sbuf(fmt, c, path);
 
 	start_reply(c, code, c->sbuf);
+	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;
 }
 
@@ -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 *me
 
 	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);
@@ -1153,6 +1206,29 @@ client_close_ev(int fd, short event, void *d)
 
 	close(c->fd);
 	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
@@ -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 
 }
 
 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)
 {
 	/*