From ee0de3613ae58a8edab93c32ba9505dcdc1c94f4 Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Thu, 27 Aug 2026 17:57:51 +0300 Subject: [PATCH] support proxypass in web server --- CMakeLists.txt | 2 +- Makefile.msvc | 2 +- Makefile.watcom | 2 +- Makefile.win | 2 +- doc/html/howtoe.html | 59 ++++++++ doc/html/howtor.html | 57 ++++++++ man/3proxy.cfg.5 | 60 ++++++++ src/auth.c | 5 +- src/httpsrv.c | 240 +++++++++++++++++++++++++++---- src/proxy.c | 10 ++ src/proxy.h | 1 + src/sockgetchar.c | 29 ++++ src/structures.h | 14 ++ tests/cases/httpsrv_proxypass.py | 203 ++++++++++++++++++++++++++ tests/harness.py | 43 ++++++ 15 files changed, 699 insertions(+), 30 deletions(-) create mode 100644 tests/cases/httpsrv_proxypass.py diff --git a/CMakeLists.txt b/CMakeLists.txt index a567b9d..fed8642 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,7 +169,7 @@ if(WIN32) endif() # Windows libraries - set(WINDOWS_LIBS ws2_32 advapi32 user32 kernel32 gdi32 crypt32) + set(WINDOWS_LIBS ws2_32 mswsock advapi32 user32 kernel32 gdi32 crypt32) # Windows plugins (always built) set(DEFAULT_PLUGINS diff --git a/Makefile.msvc b/Makefile.msvc index cae7ce5..0e4bbb5 100644 --- a/Makefile.msvc +++ b/Makefile.msvc @@ -24,7 +24,7 @@ LN = link LDFLAGS = /nologo /subsystem:console /incremental:no DLFLAGS = /DLL DLSUFFICS = .dll -LIBS = ws2_32.lib advapi32.lib odbc32.lib user32.lib kernel32.lib Gdi32.lib Crypt32.lib $(SSL_LIBS) pcre2-8.lib +LIBS = ws2_32.lib mswsock.lib advapi32.lib odbc32.lib user32.lib kernel32.lib Gdi32.lib Crypt32.lib $(SSL_LIBS) pcre2-8.lib LIBSPREFIX = LIBSSUFFIX = .lib LIBEXT = .lib diff --git a/Makefile.watcom b/Makefile.watcom index fe9faaa..00df629 100644 --- a/Makefile.watcom +++ b/Makefile.watcom @@ -14,7 +14,7 @@ LN = link LDFLAGS = /nologo /subsystem:console /incremental:no DLFLAGS = /DLL DLSUFFICS = .dll -LIBS = ws2_32.lib advapi32.lib user32.lib kernel32.lib +LIBS = ws2_32.lib mswsock.lib advapi32.lib user32.lib kernel32.lib LIBSPREFIX = LIBSSUFFIX = .lib LIBEXT = .lib diff --git a/Makefile.win b/Makefile.win index 065cb77..9d7a2dd 100644 --- a/Makefile.win +++ b/Makefile.win @@ -30,7 +30,7 @@ ifeq ($(HTTPSRV),true) endif DLFLAGS ?= -shared DLSUFFICS = .dll -LIBS += -lws2_32 -lodbc32 -ladvapi32 -luser32 -lbcrypt +LIBS += -lws2_32 -lmswsock -lodbc32 -ladvapi32 -luser32 -lbcrypt LIBSPREFIX = -l LIBSSUFFIX = LNOUT = -o diff --git a/doc/html/howtoe.html b/doc/html/howtoe.html index 0ec3ac7..0975f2b 100644 --- a/doc/html/howtoe.html +++ b/doc/html/howtoe.html @@ -867,6 +867,65 @@ http file * /** /usr/local/web/blocked.html text/html * * 403 httpsrv -p8080 -i127.0.0.1
+Both a site and a proxy. A request may arrive the way it arrives at a +site - a path, with the name in the Host header - or the way it arrives at a +proxy, naming the whole URL, or the host alone with CONNECT. Both are read. A +proxy-form request authenticates with Proxy-Authorization and is refused with +407, the way a proxy refuses one; a site-form request uses Authorization and +401. What answers it is decided by the rules either way. +
+
+proxypass is the rule which answers by fetching, so one service can
+serve what it has and proxy the rest:
+
+auth iponly +allow * +http file * /local/** "/usr/local/web/$1" +http proxypass * /** +httpsrv -p8080 ++
+An access rule redirecting to the local proxy does the same without a rule for
+it. The chain with no address is what "the local proxy" is written as, and the
+second allow is what the proxy matches on the pass it makes itself,
+since a rule carrying the chain is not taken twice:
+
+auth iponly +allow * +parent 1000 http 0.0.0.0 0 +allow * +http file * /local/** "/usr/local/web/$1" +httpsrv -p8080 ++
+Authentication happens twice for the same reason - once for the service, once +for the proxy - so a configuration which asks for credentials asks for them the +way a proxy does. +
++The access rules are read from the top on both passes, and the second one is +where the request's destination is known. On the first pass the service is +answering for itself, so an address or a port in a rule is matched against the +address the client connected to; the name from the request is matched on both +passes. On the second the destination is the one the request names, so rules +written with an address, a port or a name decide what the proxy may fetch, and +they decide it before it connects: +
+auth iponly +allow * +parent 1000 http 0.0.0.0 0 +allow * * * 80,443 +deny * +httpsrv -p8080 ++
+Everything reaches the rules, only ports 80 and 443 are fetched, and a
+deny written before the rule carrying the chain applies on both
+passes just the same. The connection to the server is kept for the next request and
+closed when that request goes elsewhere, or when the server has closed it in the
+meantime.
+
Connections. A client asking in HTTP/1.1 gets a 1.1 answer and the
connection is kept for the next request, unless it sent
Connection: close; a 1.0 client has to ask for keep-alive. The
diff --git a/doc/html/howtor.html b/doc/html/howtor.html
index 21aab93..723d299 100644
--- a/doc/html/howtor.html
+++ b/doc/html/howtor.html
@@ -877,6 +877,63 @@ http file * /** /usr/local/web/blocked.html text/html * * 403
httpsrv -p8080 -i127.0.0.1
+И сайт, и прокси. Запрос может прийти так, как приходит на сайт - путь, +имя в заголовке Host, - или так, как приходит на прокси: с полным URL, либо, для +туннеля, с одним именем хоста в CONNECT. Читается и то, и другое. Запрос в форме +для прокси аутентифицируется через Proxy-Authorization и отклоняется кодом 407, +как это делает прокси; запрос в форме для сайта - через Authorization и 401. Чем +он будет обработан, в обоих случаях решают правила. +
+
+proxypass - правило, которое отвечает, забирая ресурс, поэтому один
+сервис может отдавать своё и проксировать остальное:
+
+auth iponly +allow * +http file * /local/** "/usr/local/web/$1" +http proxypass * /** +httpsrv -p8080 ++
+Правило доступа с перенаправлением на локальный прокси делает то же самое без
+отдельного правила. Цепочка без адреса и означает "локальный прокси", а второй
+allow - то, с чем совпадает сам прокси на своём проходе, так как
+правило с цепочкой второй раз не берётся:
+
+auth iponly +allow * +parent 1000 http 0.0.0.0 0 +allow * +http file * /local/** "/usr/local/web/$1" +httpsrv -p8080 ++
+Аутентификация по той же причине происходит дважды - для сервиса и для прокси, - +поэтому конфигурация, требующая учётных данных, запрашивает их так, как это +делает прокси. +
++Правила доступа просматриваются с начала на обоих проходах, и назначение запроса +известно на втором. На первом сервис отвечает сам за себя, поэтому адрес или порт +в правиле сопоставляется с адресом, на который подключился клиент; имя из запроса +сопоставляется на обоих проходах. На втором назначение - то, которое названо в +запросе, поэтому правила с адресом, портом или именем определяют, что прокси +разрешено забрать, и определяют это до установления соединения: +
+auth iponly +allow * +parent 1000 http 0.0.0.0 0 +allow * * * 80,443 +deny * +httpsrv -p8080 ++
+До правил доходит всё, забираются только порты 80 и 443, а deny,
+записанный до правила с цепочкой, действует на обоих проходах точно так же. Соединение с сервером сохраняется для следующего запроса и
+закрывается, если следующий запрос идёт в другое место или если сервер за это
+время его закрыл.
+
Соединения. Клиент, обратившийся по HTTP/1.1, получает ответ 1.1, и
соединение сохраняется для следующего запроса, если он не прислал
Connection: close; клиенту 1.0 нужно запросить keep-alive явно.
diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5
index 98f3e01..2b22cb7 100644
--- a/man/3proxy.cfg.5
+++ b/man/3proxy.cfg.5
@@ -1626,6 +1626,9 @@ host, and the address and port it came from. For testing.
.br
\fBdata\fR \fI[size=N] [block=N] [status=N] [chunked=1] [delay=N]\fR - answer
with generated content of the size asked for. For testing.
+.br
+ \fBproxypass\fR - hand the request to the proxy code, which fetches it the
+way \fBproxy\fR would, see BOTH A SITE AND A PROXY.
.br
\fBadmin\fR, \fBadmin_counters\fR, \fBadmin_reload\fR, \fBadmin_services\fR -
the pages of the administration interface.
@@ -1693,6 +1696,63 @@ anchored with \fB$\fR \- is written in quotes. \fB$$\fR stands for a single
dollar and is not read as an include either, which is how a dollar reaches a
rule as text.
+.SS Both a site and a proxy
+A request may arrive the way it arrives at a site, naming a path and a host in
+the Host header, or the way it arrives at a proxy, naming the whole URL, or, for
+a tunnel, the host alone with CONNECT. Both are read. A request in the proxy
+form authenticates with Proxy-Authorization and is refused with 407, as a proxy
+refuses one; a request in the site form uses Authorization and 401.
+.br
+ What answers a request is still decided by the rules. \fBproxypass\fR is the
+rule which answers by fetching, so a service can serve what it has and proxy the
+rest:
+.br
+ http file * /local/** "/usr/local/web/$1"
+.br
+ http proxypass * /**
+.br
+ httpsrv -p8080
+.br
+ The same happens without a rule for it where an access rule redirects to the
+local proxy, which is written as a chain of no address: the rules are asked
+first, and a request none of them answers is fetched.
+.br
+ allow *
+.br
+ parent 1000 http 0.0.0.0 0
+.br
+ allow *
+.br
+ The second \fBallow\fR is what the proxy matches on the pass it makes itself:
+a rule carrying the chain is not taken twice. Authentication happens twice for
+the same reason, once for the service and once for the proxy, so a configuration
+asking for credentials asks for them as a proxy does.
+.br
+ The access rules are read from the top on both passes, and it is the second
+pass which describes where the request is going. On the first one the service is
+answering for itself, so the destination an address or a port is matched against
+is the address the client connected to; the name from the request is matched on
+both. On the second the destination is the one the request names, so rules
+written with an address, a port or a name decide what the proxy is allowed to
+fetch, and they decide it before the connection is made:
+.br
+ allow *
+.br
+ parent 1000 http 0.0.0.0 0
+.br
+ allow * * * 80,443
+.br
+ deny *
+.br
+ Everything reaches the rules, and only ports 80 and 443 are fetched. A rule
+before the one carrying the chain applies on both passes just the same, so a
+\fBdeny\fR written there stops the request as well.
+.br
+ The connection to the server is kept for the request after it, and closed when
+the request after it goes somewhere else, or when the server has closed it in
+the meantime. A tunnel is fetched by the proxy code as well, which means the
+connection carrying it belongs to that request alone.
+
.SS Connections
An answer is sent as HTTP/1.1 to a client which asked in HTTP/1.1, and the
connection is kept for the next request unless the client sent
diff --git a/src/auth.c b/src/auth.c
index a4daedb..e37c783 100644
--- a/src/auth.c
+++ b/src/auth.c
@@ -20,8 +20,9 @@ int alwaysauth(struct clientparam * param){
if(conf.connlimiter && !param->connlim && startconnlims(param)) return 10;
#ifdef WITH_HTTPSRV
/* The http server answers the request itself, so authorization must not
- try to reach a destination that does not exist. */
- res = (param->srv->service == S_HTTPSRV)? 0 : doconnect(param);
+ try to reach a destination that does not exist. A request it has handed
+ to another child does have one, and that child needs it opened. */
+ res = (param->srv->service == S_HTTPSRV && !param->onerequest)? 0 : doconnect(param);
#else
res = doconnect(param);
#endif
diff --git a/src/httpsrv.c b/src/httpsrv.c
index e037d38..de2b6ec 100644
--- a/src/httpsrv.c
+++ b/src/httpsrv.c
@@ -54,6 +54,8 @@
after it should be tried again. */
#define HTTPSRV_REWRITTEN 2
#define HTTPSRV_MAXBODY 1048576
+/* the most of a request this server keeps in case it hands it on */
+#define HTTPSRV_MAXRAW 65536
/* lengths a reply is written with: a count, or one of these */
#define HTTPSRV_LEN_CHUNKED (-1)
#define HTTPSRV_LEN_NONE (-2)
@@ -1035,11 +1037,14 @@ static int op_authrequired(struct httpreq *r)
r->hdrs = NULL;
r->maxage = -1;
r->keepalive = 0;
- if(httpsrv_printf(r, "HTTP/1.0 401 Authentication Required\r\n"
- "WWW-Authenticate: Basic realm=\"3proxy\"\r\n"
+ if(httpsrv_printf(r, "HTTP/1.0 %s\r\n"
+ "%s: Basic realm=\"3proxy\"\r\n"
"Content-Type: text/plain\r\n"
"Content-Length: %d\r\n"
- "Connection: close\r\n\r\n", (int)sizeof(body) - 1)) return 1;
+ "Connection: close\r\n\r\n",
+ r->proxy? "407 Proxy Authentication Required" : "401 Authentication Required",
+ r->proxy? "Proxy-Authenticate" : "WWW-Authenticate",
+ (int)sizeof(body) - 1)) return 1;
return httpsrv_send(r, body, (int)sizeof(body) - 1);
}
@@ -1079,20 +1084,22 @@ static struct httpop {
const char *name;
int (*fn)(struct httpreq *, const unsigned char *params);
int framed; /* the answer says how long it is, so the connection may be kept */
+ int handoff; /* the request is answered by the proxy code, not here */
} httpops[] = {
- {"echo", op_echo, 1},
- {"data", op_data, 1},
- {"file", op_file, 1},
- {"cache", op_cache, 1},
- {"redir", op_redir, 1},
- {"reply", op_reply, 1},
- {"rewrite", op_rewrite, 1},
- {"rewrite_host", op_rewrite_host, 1},
- {"admin", op_admin, 0},
- {"admin_counters", op_admin_counters, 0},
- {"admin_reload", op_admin_reload, 0},
- {"admin_services", op_admin_services, 0},
- {NULL, NULL, 0}
+ {"echo", op_echo, 1, 0},
+ {"data", op_data, 1, 0},
+ {"file", op_file, 1, 0},
+ {"cache", op_cache, 1, 0},
+ {"redir", op_redir, 1, 0},
+ {"reply", op_reply, 1, 0},
+ {"rewrite", op_rewrite, 1, 0},
+ {"rewrite_host", op_rewrite_host, 1, 0},
+ {"admin", op_admin, 0, 0},
+ {"admin_counters", op_admin_counters, 0, 0},
+ {"admin_reload", op_admin_reload, 0, 0},
+ {"admin_services", op_admin_services, 0, 0},
+ {"proxypass", NULL, 0, 1},
+ {NULL, NULL, 0, 0}
};
void freehttprules(struct httprule *rule)
@@ -1143,12 +1150,100 @@ static void httpsrv_drain(struct clientparam *param, uint64_t len)
}
}
+/* Keeps the request as the client wrote it. Only what a handoff needs: the
+ request line and the headers, exactly as they came, since the proxy code
+ reads them again from the beginning. */
+static int rawkeep(struct httpreq *r, const char *line, int len)
+{
+ if(r->rawlen + len + 3 > r->rawsize){
+ int want = r->rawsize? r->rawsize * 2 : 2048;
+ unsigned char *grown;
+
+ while(want < r->rawlen + len + 3) want *= 2;
+ if(want > HTTPSRV_MAXRAW) return 1;
+ grown = realloc(r->raw, (size_t)want);
+ if(!grown) return 1;
+ r->raw = grown;
+ r->rawsize = want;
+ }
+ memcpy(r->raw + r->rawlen, line, (size_t)len);
+ r->rawlen += len;
+ return 0;
+}
+
+/* Hands the request to the proxy code, which reads it again from the client
+ buffer and answers it as a proxy would, asking for its own credentials if
+ the configuration wants them. The connection stays with this service. */
+static int proxypass(struct clientparam *param, struct httpreq *r)
+{
+ void *next;
+ int state, stale = 0;
+
+ /* This is the one place a child is called rather than returned, so it
+ is the one place a chain of them could nest. It cannot: the proxy
+ returns whatever child it would redirect to instead of calling it, and
+ a request already being answered on behalf of another child is never
+ handed on again. */
+ if(param->onerequest) return 1;
+ if(!r->raw || rawkeep(r, "\r\n", 2)) return 1; /* the end of the headers */
+
+ /* What is open towards the server belongs to the request before this
+ one. It is no use for this request if it went somewhere else, and no
+ use at all if the server has since closed it: the proxy watches for
+ that between its own requests, and this is where that falls to when it
+ is entered one request at a time. */
+ if(param->remsock != INVALID_SOCKET){
+ struct pollfd fd;
+
+ memset(&fd, 0, sizeof(fd));
+ fd.fd = param->remsock;
+ fd.events = POLLIN;
+ if(param->srv->so._poll(param->sostate, &fd, 1, 0) > 0
+ && (fd.revents & (POLLIN|POLLHUP|POLLERR|POLLNVAL))){
+ /* anything arriving now belongs to no request */
+ stale = 1;
+ }
+ }
+ if(param->remsock != INVALID_SOCKET && (stale || (r->lasthost
+ && strcasecmp(r->lasthost, r->host)))){
+ param->srv->so._shutdown(param->sostate, param->remsock, SHUT_RDWR);
+ param->srv->so._closesocket(param->sostate, param->remsock);
+ param->remsock = INVALID_SOCKET;
+ param->redirected = 0;
+ param->redirtype = 0;
+ memset(¶m->sinsl, 0, sizeof(param->sinsl));
+ memset(¶m->sinsr, 0, sizeof(param->sinsr));
+ memset(¶m->req, 0, sizeof(param->req));
+ }
+ if(r->lasthost){
+ strncpy(r->lasthost, r->host, 255);
+ r->lasthost[255] = 0;
+ }
+
+ if(pushbackcli(param, r->raw, r->rawlen)) return 1;
+
+ param->onerequest = 1;
+ next = proxychild(param);
+ state = param->onerequest;
+ param->onerequest = 0;
+
+ /* The proxy asked for another child to take the connection over: it is
+ no longer this service's to keep. */
+ if(next){
+ r->handoff = next;
+ return 2;
+ }
+ if(state != 2) r->keepalive = 0;
+ return 0;
+}
+
/* Reads one request and answers it. Returns 0 when nothing more came on a
connection which was being kept open, which is not a request and not an
error, so there is nothing to answer and nothing to log. */
static int httpsrv_request(struct clientparam *param, struct httpreq *r)
{
char buf[HTTPSRV_LINE];
+ char rootpath[2];
char *sp, *q;
struct httprule *rule;
int i, hdrs = 0;
@@ -1157,6 +1252,7 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
conf.timeouts[STRING_S]);
if(i <= 0 && !r->first) return 0; /* the client is done with us */
if(i < 5) RETURN(701);
+ if(rawkeep(r, buf, i)) RETURN(710);
buf[i] = 0;
sp = strchr(buf, ' ');
@@ -1186,6 +1282,35 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
when the client asks for it. */
r->keepalive = r->version;
+ /* A client talking to a proxy names the whole URL, or, for a tunnel, the
+ host alone. The name in the request is the one that counts then, and
+ the credentials arrive in Proxy-Authorization, because the client is
+ identifying itself to a proxy and not to a site. */
+ if(!strncasecmp(sp, "http://", 7)){
+ char *slash;
+
+ r->proxy = 1;
+ sp += 7;
+ slash = strchr(sp, '/');
+ if(slash) *slash = 0;
+ if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706);
+ if(slash){
+ *slash = '/';
+ sp = slash;
+ }
+ else {
+ strcpy(rootpath, "/");
+ sp = rootpath;
+ }
+ }
+ else if(!strcasecmp(r->method, "CONNECT")){
+ r->proxy = r->connect = 1;
+ if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706);
+ strcpy(rootpath, "/");
+ sp = rootpath;
+ }
+ else if(*sp != '/') RETURN(702);
+
q = strchr(sp, '?');
if(q){
*q = 0;
@@ -1205,18 +1330,20 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
while(hdrs++ < HTTPSRV_MAXHDR &&
(i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1,
'\n', conf.timeouts[STRING_S])) > 2){
+ if(rawkeep(r, buf, i)) RETURN(710);
buf[i] = 0;
- if(!strncasecmp(buf, "host:", 5)){
+ if(!strncasecmp(buf, "host:", 5) && !r->proxy){
sp = buf + 5;
while(isspace((unsigned char)*sp)) sp++;
sp[strcspn(sp, "\r\n")] = 0;
if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706);
}
- else if(!strncasecmp(buf, "authorization:", 14)){
+ else if((!r->proxy && !strncasecmp(buf, "authorization:", 14))
+ || (r->proxy && !strncasecmp(buf, "proxy-authorization:", 20))){
char creds[256];
int clen;
- sp = buf + 14;
+ sp = buf + (r->proxy? 20 : 14);
while(isspace((unsigned char)*sp)) sp++;
if(strncasecmp(sp, "basic", 5)) continue;
sp += 5;
@@ -1259,9 +1386,11 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
/* The next request begins where this body ends, so a body which cannot
be read to its end - one this server does not frame, or one longer
- than it is willing to read - closes the connection instead. */
+ than it is willing to read - closes the connection instead.
+
+ The body itself is left where it is until this server knows it is the
+ one answering: a request handed to the proxy carries its body there. */
if(r->chunkedreq || r->contentlen > HTTPSRV_MAXBODY) r->keepalive = 0;
- if(r->contentlen) httpsrv_drain(param, r->contentlen);
if(r->host[0]){
char host[sizeof(r->host)];
@@ -1291,6 +1420,24 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
param->req = param->sincl;
i = (*param->srv->authfunc)(param);
+ /* A rule which redirects is answered by another child: authorization
+ names it and reports success, or, where the destination was not needed
+ to decide, reports the redirect itself. Where that child is the local
+ proxy, this server still answers whatever it has a rule for and lets
+ the proxy have the rest, which is what makes one service both a site
+ and a proxy. Any other child takes the connection over as it always
+ has. */
+ if(i == REDIRECT) i = 0;
+ if(!i && param->redirectfunc){
+ if(param->redirectfunc == (REDIRECTFUNC)proxychild) r->mayproxy = 1;
+ /* A redirect back to this service would only ask the same rules
+ the same question, so it is left alone rather than bounced
+ between children until the count runs out. */
+ else if(param->redirectfunc != (REDIRECTFUNC)httpsrvchild){
+ r->handoff = (void *)param->redirectfunc;
+ return 1;
+ }
+ }
if(i && i != 10){
/* 4 no credentials, 5 unknown user, 6 wrong password: all of them
should let the client offer credentials again. */
@@ -1313,6 +1460,25 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
r->globstart = r->ncaps > 1? r->caps[1].start : 0;
r->globlen = r->ncaps > 1? r->caps[1].len : 0;
+ /* A rule which hands the request on answers nothing itself, and
+ the body has to still be there when it does. */
+ if(httpops[rule->op].handoff){
+ i = proxypass(param, r);
+ if(i == 1){
+ r->keepalive = 0;
+ op_badrequest(r);
+ RETURN(711);
+ }
+ RETURN(0);
+ }
+
+ /* This server is answering, so the body is read and thrown away
+ before the answer goes out. */
+ if(!r->drained){
+ if(r->contentlen) httpsrv_drain(param, r->contentlen);
+ r->drained = 1;
+ }
+
/* Only an answer which says how long it is may be followed by
another request on the same connection. */
if(!httpops[rule->op].framed) r->keepalive = 0;
@@ -1332,6 +1498,21 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
op_badrequest(r);
RETURN(709);
}
+ /* Nothing here answers it. An access rule may have said the local proxy
+ should, which is what makes a service both a site and a proxy. */
+ if(r->mayproxy){
+ i = proxypass(param, r);
+ if(i == 1){
+ r->keepalive = 0;
+ op_badrequest(r);
+ RETURN(711);
+ }
+ RETURN(0);
+ }
+ if(!r->drained){
+ if(r->contentlen) httpsrv_drain(param, r->contentlen);
+ r->drained = 1;
+ }
op_notfound(r);
RETURN(404);
@@ -1347,16 +1528,25 @@ CLEANRET:
void * httpsrvchild(struct clientparam *param)
{
struct httpreq r;
+ char lasthost[256];
+ void *handoff = NULL;
int first = 1;
+ lasthost[0] = 0;
for(;;){
+ int answered;
+
memset(&r, 0, sizeof(r));
r.maxage = -1; /* until a rule says otherwise */
r.param = param;
r.first = first;
+ r.lasthost = lasthost;
param->res = 0;
- if(!httpsrv_request(param, &r)) break;
+ answered = httpsrv_request(param, &r);
+ handoff = r.handoff;
+ if(r.raw) free(r.raw);
+ if(!answered) break;
/* Log the request the way the proxy does: the parameters decide
what was served, so a bare path is not enough to explain a
@@ -1371,10 +1561,12 @@ void * httpsrvchild(struct clientparam *param)
dolog(param, (unsigned char *)logbuf);
}
- if(!r.keepalive) break;
+ if(handoff || !r.keepalive) break;
first = 0;
}
- return NULL;
+ /* A child named by an access rule takes the connection over, which the
+ caller arranges rather than this service calling it. */
+ return handoff;
}
#endif
diff --git a/src/proxy.c b/src/proxy.c
index da0585e..83a0a32 100644
--- a/src/proxy.c
+++ b/src/proxy.c
@@ -1271,6 +1271,16 @@ REQUESTEND:
RETURN(0);
}
if(param->transparent && (!ckeepalive || !keepalive)) {RETURN (0);}
+ /* Another service read this request and handed it here to be answered. It
+ keeps the connection and decides what the next request on it is, so this
+ one is done. Whatever was opened towards the server stays open in param
+ for the next one. */
+ if(param->onerequest){
+ /* 2 says the client connection may carry another request, 1 that it may
+ not, which is what the service holding it needs to know. */
+ param->onerequest = (ckeepalive && keepalive)? 2 : 1;
+ RETURN(0);
+ }
logurl(param, (char *)buf, (char *)req, ftp);
param->status = 0;
diff --git a/src/proxy.h b/src/proxy.h
index d4ac342..837f9b8 100644
--- a/src/proxy.h
+++ b/src/proxy.h
@@ -395,6 +395,7 @@ void pcre_pattern_free(void *re);
int pcre_pattern_match(void *re, const unsigned char *subject, struct capture *caps, int maxcaps);
#endif
+int pushbackcli(struct clientparam * param, const unsigned char * data, int len);
int parsepattern(struct hostname *h, unsigned char *arg);
int parsepathpattern(struct hostname *h, unsigned char *arg);
int patternmatchcaps(const struct hostname *h, const unsigned char *str,
diff --git a/src/sockgetchar.c b/src/sockgetchar.c
index cd76351..2e0ae0e 100644
--- a/src/sockgetchar.c
+++ b/src/sockgetchar.c
@@ -88,6 +88,35 @@ int sockgetcharcli(struct clientparam * param, int timeosec, int timeousec){
return (int)*param->clibuf;
}
+/* Put bytes back in front of whatever the client has not been read yet, so a
+ service which has already taken a request off the socket can hand it to
+ another one, which reads it the way it reads anything else. */
+int pushbackcli(struct clientparam * param, const unsigned char * data, int len){
+ unsigned left = 0;
+ unsigned need;
+
+ if(len <= 0) return 0;
+ if(param->clibuf) left = param->cliinbuf - param->clioffset;
+ need = (unsigned)len + left;
+
+ if(!param->clibuf){
+ if(!(param->clibuf = malloc(need > SRVBUFSIZE? need : SRVBUFSIZE))) return 1;
+ param->clibufsize = need > SRVBUFSIZE? need : SRVBUFSIZE;
+ }
+ else if(param->clibufsize < need){
+ unsigned char *nb = realloc(param->clibuf, need);
+
+ if(!nb) return 1;
+ param->clibuf = nb;
+ param->clibufsize = need;
+ }
+ if(left) memmove(param->clibuf + len, param->clibuf + param->clioffset, left);
+ memcpy(param->clibuf, data, (size_t)len);
+ param->clioffset = 0;
+ param->cliinbuf = need;
+ return 0;
+}
+
unsigned long sockfillbuffcli(struct clientparam * param, unsigned long size, int timeosec){
int len;
diff --git a/src/structures.h b/src/structures.h
index a76f7c6..f7a9d42 100644
--- a/src/structures.h
+++ b/src/structures.h
@@ -385,6 +385,14 @@ struct httpreq {
int keepalive; /* whether the connection carries another request */
int first; /* the first request on this connection */
int chunkedreq; /* a body this server does not know how to read */
+ int proxy; /* the client asked the way it asks a proxy */
+ int connect; /* and asked for a tunnel */
+ int mayproxy; /* an access rule sent this to the local proxy */
+ unsigned char *raw; /* the request as it arrived, for handing on */
+ int rawlen, rawsize;
+ int drained; /* the body has been read and thrown away */
+ char *lasthost; /* where the last request on this connection went */
+ void *handoff; /* a child which takes the connection over */
struct clientparam *param;
char method[16];
char path[256];
@@ -745,6 +753,12 @@ struct clientparam {
int udp_nhops;
struct ace *lastace;
time_t time_start;
+ /* Set by a service which read a request itself and handed it to another
+ child to answer: that child answers this one request and returns,
+ leaving the connection to the service which called it. Added last so
+ that a plugin built against an older header still finds the fields it
+ knows where they were. */
+ int onerequest;
};
struct filemon {
diff --git a/tests/cases/httpsrv_proxypass.py b/tests/cases/httpsrv_proxypass.py
new file mode 100644
index 0000000..a41bd92
--- /dev/null
+++ b/tests/cases/httpsrv_proxypass.py
@@ -0,0 +1,203 @@
+"""A service which is both a site and a proxy.
+
+The rules answer what they have; anything else is handed to the proxy code,
+which authenticates as a proxy and fetches it. The same connection carries
+both kinds of request.
+"""
+
+import os
+
+
+def run(t):
+ root = os.path.join(t.tmpdir, "pp")
+ os.makedirs(root, exist_ok=True)
+ with open(os.path.join(root, "a.html"), "w") as fp:
+ fp.write("