support proxypass in web server

This commit is contained in:
Vladimir Dubrovin 2026-08-27 17:57:51 +03:00
parent 8971fcf991
commit ee0de3613a
15 changed files with 699 additions and 30 deletions

View File

@ -169,7 +169,7 @@ if(WIN32)
endif() endif()
# Windows libraries # 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) # Windows plugins (always built)
set(DEFAULT_PLUGINS set(DEFAULT_PLUGINS

View File

@ -24,7 +24,7 @@ LN = link
LDFLAGS = /nologo /subsystem:console /incremental:no LDFLAGS = /nologo /subsystem:console /incremental:no
DLFLAGS = /DLL DLFLAGS = /DLL
DLSUFFICS = .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 = LIBSPREFIX =
LIBSSUFFIX = .lib LIBSSUFFIX = .lib
LIBEXT = .lib LIBEXT = .lib

View File

@ -14,7 +14,7 @@ LN = link
LDFLAGS = /nologo /subsystem:console /incremental:no LDFLAGS = /nologo /subsystem:console /incremental:no
DLFLAGS = /DLL DLFLAGS = /DLL
DLSUFFICS = .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 = LIBSPREFIX =
LIBSSUFFIX = .lib LIBSSUFFIX = .lib
LIBEXT = .lib LIBEXT = .lib

View File

@ -30,7 +30,7 @@ ifeq ($(HTTPSRV),true)
endif endif
DLFLAGS ?= -shared DLFLAGS ?= -shared
DLSUFFICS = .dll DLSUFFICS = .dll
LIBS += -lws2_32 -lodbc32 -ladvapi32 -luser32 -lbcrypt LIBS += -lws2_32 -lmswsock -lodbc32 -ladvapi32 -luser32 -lbcrypt
LIBSPREFIX = -l LIBSPREFIX = -l
LIBSSUFFIX = LIBSSUFFIX =
LNOUT = -o LNOUT = -o

View File

@ -867,6 +867,65 @@ http file * /** /usr/local/web/blocked.html text/html * * 403
httpsrv -p8080 -i127.0.0.1 httpsrv -p8080 -i127.0.0.1
</pre> </pre>
<p> <p>
<b>Both a site and a proxy.</b> 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.
</p>
<p>
<code>proxypass</code> is the rule which answers by fetching, so one service can
serve what it has and proxy the rest:
</p><pre>
auth iponly
allow *
http file * /local/** "/usr/local/web/$1"
http proxypass * /**
httpsrv -p8080
</pre>
<p>
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 <code>allow</code> is what the proxy matches on the pass it makes itself,
since a rule carrying the chain is not taken twice:
</p><pre>
auth iponly
allow *
parent 1000 http 0.0.0.0 0
allow *
http file * /local/** "/usr/local/web/$1"
httpsrv -p8080
</pre>
<p>
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.
</p>
<p>
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:
</p><pre>
auth iponly
allow *
parent 1000 http 0.0.0.0 0
allow * * * 80,443
deny *
httpsrv -p8080
</pre>
<p>
Everything reaches the rules, only ports 80 and 443 are fetched, and a
<code>deny</code> 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.
</p>
<p>
<b>Connections.</b> A client asking in HTTP/1.1 gets a 1.1 answer and the <b>Connections.</b> 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 is kept for the next request, unless it sent
<code>Connection: close</code>; a 1.0 client has to ask for keep-alive. The <code>Connection: close</code>; a 1.0 client has to ask for keep-alive. The

View File

@ -877,6 +877,63 @@ http file * /** /usr/local/web/blocked.html text/html * * 403
httpsrv -p8080 -i127.0.0.1 httpsrv -p8080 -i127.0.0.1
</pre> </pre>
<p> <p>
<b>И сайт, и прокси.</b> Запрос может прийти так, как приходит на сайт - путь,
имя в заголовке Host, - или так, как приходит на прокси: с полным URL, либо, для
туннеля, с одним именем хоста в CONNECT. Читается и то, и другое. Запрос в форме
для прокси аутентифицируется через Proxy-Authorization и отклоняется кодом 407,
как это делает прокси; запрос в форме для сайта - через Authorization и 401. Чем
он будет обработан, в обоих случаях решают правила.
</p>
<p>
<code>proxypass</code> - правило, которое отвечает, забирая ресурс, поэтому один
сервис может отдавать своё и проксировать остальное:
</p><pre>
auth iponly
allow *
http file * /local/** "/usr/local/web/$1"
http proxypass * /**
httpsrv -p8080
</pre>
<p>
Правило доступа с перенаправлением на локальный прокси делает то же самое без
отдельного правила. Цепочка без адреса и означает "локальный прокси", а второй
<code>allow</code> - то, с чем совпадает сам прокси на своём проходе, так как
правило с цепочкой второй раз не берётся:
</p><pre>
auth iponly
allow *
parent 1000 http 0.0.0.0 0
allow *
http file * /local/** "/usr/local/web/$1"
httpsrv -p8080
</pre>
<p>
Аутентификация по той же причине происходит дважды - для сервиса и для прокси, -
поэтому конфигурация, требующая учётных данных, запрашивает их так, как это
делает прокси.
</p>
<p>
Правила доступа просматриваются с начала на обоих проходах, и назначение запроса
известно на втором. На первом сервис отвечает сам за себя, поэтому адрес или порт
в правиле сопоставляется с адресом, на который подключился клиент; имя из запроса
сопоставляется на обоих проходах. На втором назначение - то, которое названо в
запросе, поэтому правила с адресом, портом или именем определяют, что прокси
разрешено забрать, и определяют это до установления соединения:
</p><pre>
auth iponly
allow *
parent 1000 http 0.0.0.0 0
allow * * * 80,443
deny *
httpsrv -p8080
</pre>
<p>
До правил доходит всё, забираются только порты 80 и 443, а <code>deny</code>,
записанный до правила с цепочкой, действует на обоих проходах точно так же. Соединение с сервером сохраняется для следующего запроса и
закрывается, если следующий запрос идёт в другое место или если сервер за это
время его закрыл.
</p>
<p>
<b>Соединения.</b> Клиент, обратившийся по HTTP/1.1, получает ответ 1.1, и <b>Соединения.</b> Клиент, обратившийся по HTTP/1.1, получает ответ 1.1, и
соединение сохраняется для следующего запроса, если он не прислал соединение сохраняется для следующего запроса, если он не прислал
<code>Connection: close</code>; клиенту 1.0 нужно запросить keep-alive явно. <code>Connection: close</code>; клиенту 1.0 нужно запросить keep-alive явно.

View File

@ -1626,6 +1626,9 @@ host, and the address and port it came from. For testing.
.br .br
\fBdata\fR \fI[size=N] [block=N] [status=N] [chunked=1] [delay=N]\fR - answer \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. 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 .br
\fBadmin\fR, \fBadmin_counters\fR, \fBadmin_reload\fR, \fBadmin_services\fR - \fBadmin\fR, \fBadmin_counters\fR, \fBadmin_reload\fR, \fBadmin_services\fR -
the pages of the administration interface. 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 dollar and is not read as an include either, which is how a dollar reaches a
rule as text. 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 .SS Connections
An answer is sent as HTTP/1.1 to a client which asked in HTTP/1.1, and the 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 connection is kept for the next request unless the client sent

View File

@ -20,8 +20,9 @@ int alwaysauth(struct clientparam * param){
if(conf.connlimiter && !param->connlim && startconnlims(param)) return 10; if(conf.connlimiter && !param->connlim && startconnlims(param)) return 10;
#ifdef WITH_HTTPSRV #ifdef WITH_HTTPSRV
/* The http server answers the request itself, so authorization must not /* The http server answers the request itself, so authorization must not
try to reach a destination that does not exist. */ try to reach a destination that does not exist. A request it has handed
res = (param->srv->service == S_HTTPSRV)? 0 : doconnect(param); to another child does have one, and that child needs it opened. */
res = (param->srv->service == S_HTTPSRV && !param->onerequest)? 0 : doconnect(param);
#else #else
res = doconnect(param); res = doconnect(param);
#endif #endif

View File

@ -54,6 +54,8 @@
after it should be tried again. */ after it should be tried again. */
#define HTTPSRV_REWRITTEN 2 #define HTTPSRV_REWRITTEN 2
#define HTTPSRV_MAXBODY 1048576 #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 */ /* lengths a reply is written with: a count, or one of these */
#define HTTPSRV_LEN_CHUNKED (-1) #define HTTPSRV_LEN_CHUNKED (-1)
#define HTTPSRV_LEN_NONE (-2) #define HTTPSRV_LEN_NONE (-2)
@ -1035,11 +1037,14 @@ static int op_authrequired(struct httpreq *r)
r->hdrs = NULL; r->hdrs = NULL;
r->maxage = -1; r->maxage = -1;
r->keepalive = 0; r->keepalive = 0;
if(httpsrv_printf(r, "HTTP/1.0 401 Authentication Required\r\n" if(httpsrv_printf(r, "HTTP/1.0 %s\r\n"
"WWW-Authenticate: Basic realm=\"3proxy\"\r\n" "%s: Basic realm=\"3proxy\"\r\n"
"Content-Type: text/plain\r\n" "Content-Type: text/plain\r\n"
"Content-Length: %d\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); return httpsrv_send(r, body, (int)sizeof(body) - 1);
} }
@ -1079,20 +1084,22 @@ static struct httpop {
const char *name; const char *name;
int (*fn)(struct httpreq *, const unsigned char *params); int (*fn)(struct httpreq *, const unsigned char *params);
int framed; /* the answer says how long it is, so the connection may be kept */ 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[] = { } httpops[] = {
{"echo", op_echo, 1}, {"echo", op_echo, 1, 0},
{"data", op_data, 1}, {"data", op_data, 1, 0},
{"file", op_file, 1}, {"file", op_file, 1, 0},
{"cache", op_cache, 1}, {"cache", op_cache, 1, 0},
{"redir", op_redir, 1}, {"redir", op_redir, 1, 0},
{"reply", op_reply, 1}, {"reply", op_reply, 1, 0},
{"rewrite", op_rewrite, 1}, {"rewrite", op_rewrite, 1, 0},
{"rewrite_host", op_rewrite_host, 1}, {"rewrite_host", op_rewrite_host, 1, 0},
{"admin", op_admin, 0}, {"admin", op_admin, 0, 0},
{"admin_counters", op_admin_counters, 0}, {"admin_counters", op_admin_counters, 0, 0},
{"admin_reload", op_admin_reload, 0}, {"admin_reload", op_admin_reload, 0, 0},
{"admin_services", op_admin_services, 0}, {"admin_services", op_admin_services, 0, 0},
{NULL, NULL, 0} {"proxypass", NULL, 0, 1},
{NULL, NULL, 0, 0}
}; };
void freehttprules(struct httprule *rule) 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(&param->sinsl, 0, sizeof(param->sinsl));
memset(&param->sinsr, 0, sizeof(param->sinsr));
memset(&param->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 /* 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 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. */ error, so there is nothing to answer and nothing to log. */
static int httpsrv_request(struct clientparam *param, struct httpreq *r) static int httpsrv_request(struct clientparam *param, struct httpreq *r)
{ {
char buf[HTTPSRV_LINE]; char buf[HTTPSRV_LINE];
char rootpath[2];
char *sp, *q; char *sp, *q;
struct httprule *rule; struct httprule *rule;
int i, hdrs = 0; int i, hdrs = 0;
@ -1157,6 +1252,7 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
conf.timeouts[STRING_S]); conf.timeouts[STRING_S]);
if(i <= 0 && !r->first) return 0; /* the client is done with us */ if(i <= 0 && !r->first) return 0; /* the client is done with us */
if(i < 5) RETURN(701); if(i < 5) RETURN(701);
if(rawkeep(r, buf, i)) RETURN(710);
buf[i] = 0; buf[i] = 0;
sp = strchr(buf, ' '); sp = strchr(buf, ' ');
@ -1186,6 +1282,35 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
when the client asks for it. */ when the client asks for it. */
r->keepalive = r->version; 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, '?'); q = strchr(sp, '?');
if(q){ if(q){
*q = 0; *q = 0;
@ -1205,18 +1330,20 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
while(hdrs++ < HTTPSRV_MAXHDR && while(hdrs++ < HTTPSRV_MAXHDR &&
(i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1, (i = sockgetlinebuf(param, CLIENT, (unsigned char *)buf, sizeof(buf) - 1,
'\n', conf.timeouts[STRING_S])) > 2){ '\n', conf.timeouts[STRING_S])) > 2){
if(rawkeep(r, buf, i)) RETURN(710);
buf[i] = 0; buf[i] = 0;
if(!strncasecmp(buf, "host:", 5)){ if(!strncasecmp(buf, "host:", 5) && !r->proxy){
sp = buf + 5; sp = buf + 5;
while(isspace((unsigned char)*sp)) sp++; while(isspace((unsigned char)*sp)) sp++;
sp[strcspn(sp, "\r\n")] = 0; sp[strcspn(sp, "\r\n")] = 0;
if(copyfield(r->host, sizeof(r->host), sp)) RETURN(706); 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]; char creds[256];
int clen; int clen;
sp = buf + 14; sp = buf + (r->proxy? 20 : 14);
while(isspace((unsigned char)*sp)) sp++; while(isspace((unsigned char)*sp)) sp++;
if(strncasecmp(sp, "basic", 5)) continue; if(strncasecmp(sp, "basic", 5)) continue;
sp += 5; 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 /* 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 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->chunkedreq || r->contentlen > HTTPSRV_MAXBODY) r->keepalive = 0;
if(r->contentlen) httpsrv_drain(param, r->contentlen);
if(r->host[0]){ if(r->host[0]){
char host[sizeof(r->host)]; char host[sizeof(r->host)];
@ -1291,6 +1420,24 @@ static int httpsrv_request(struct clientparam *param, struct httpreq *r)
param->req = param->sincl; param->req = param->sincl;
i = (*param->srv->authfunc)(param); 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){ if(i && i != 10){
/* 4 no credentials, 5 unknown user, 6 wrong password: all of them /* 4 no credentials, 5 unknown user, 6 wrong password: all of them
should let the client offer credentials again. */ 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->globstart = r->ncaps > 1? r->caps[1].start : 0;
r->globlen = r->ncaps > 1? r->caps[1].len : 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 /* Only an answer which says how long it is may be followed by
another request on the same connection. */ another request on the same connection. */
if(!httpops[rule->op].framed) r->keepalive = 0; 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); op_badrequest(r);
RETURN(709); 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); op_notfound(r);
RETURN(404); RETURN(404);
@ -1347,16 +1528,25 @@ CLEANRET:
void * httpsrvchild(struct clientparam *param) void * httpsrvchild(struct clientparam *param)
{ {
struct httpreq r; struct httpreq r;
char lasthost[256];
void *handoff = NULL;
int first = 1; int first = 1;
lasthost[0] = 0;
for(;;){ for(;;){
int answered;
memset(&r, 0, sizeof(r)); memset(&r, 0, sizeof(r));
r.maxage = -1; /* until a rule says otherwise */ r.maxage = -1; /* until a rule says otherwise */
r.param = param; r.param = param;
r.first = first; r.first = first;
r.lasthost = lasthost;
param->res = 0; 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 /* Log the request the way the proxy does: the parameters decide
what was served, so a bare path is not enough to explain a 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); dolog(param, (unsigned char *)logbuf);
} }
if(!r.keepalive) break; if(handoff || !r.keepalive) break;
first = 0; 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 #endif

View File

@ -1271,6 +1271,16 @@ REQUESTEND:
RETURN(0); RETURN(0);
} }
if(param->transparent && (!ckeepalive || !keepalive)) {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); logurl(param, (char *)buf, (char *)req, ftp);
param->status = 0; param->status = 0;

View File

@ -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); int pcre_pattern_match(void *re, const unsigned char *subject, struct capture *caps, int maxcaps);
#endif #endif
int pushbackcli(struct clientparam * param, const unsigned char * data, int len);
int parsepattern(struct hostname *h, unsigned char *arg); int parsepattern(struct hostname *h, unsigned char *arg);
int parsepathpattern(struct hostname *h, unsigned char *arg); int parsepathpattern(struct hostname *h, unsigned char *arg);
int patternmatchcaps(const struct hostname *h, const unsigned char *str, int patternmatchcaps(const struct hostname *h, const unsigned char *str,

View File

@ -88,6 +88,35 @@ int sockgetcharcli(struct clientparam * param, int timeosec, int timeousec){
return (int)*param->clibuf; 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){ unsigned long sockfillbuffcli(struct clientparam * param, unsigned long size, int timeosec){
int len; int len;

View File

@ -385,6 +385,14 @@ struct httpreq {
int keepalive; /* whether the connection carries another request */ int keepalive; /* whether the connection carries another request */
int first; /* the first request on this connection */ int first; /* the first request on this connection */
int chunkedreq; /* a body this server does not know how to read */ 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; struct clientparam *param;
char method[16]; char method[16];
char path[256]; char path[256];
@ -745,6 +753,12 @@ struct clientparam {
int udp_nhops; int udp_nhops;
struct ace *lastace; struct ace *lastace;
time_t time_start; 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 { struct filemon {

View File

@ -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("<h1>local</h1>")
# two origins, so a change of destination is visible
one = t.free_port()
two = t.free_port()
t.start("httpsrv_proxypass_origins", f"""
auth iponly
allow *
http echo * /**
httpsrv -p{one}
flush
auth iponly
allow *
http echo * /**
httpsrv -p{two}
""", ports=[one, two])
# --- the rule which hands a request on ---------------------------------
srv = t.free_port()
t.start("httpsrv_proxypass", f"""
log
auth iponly
allow *
http file * /local/*.html "{root}/$1.html"
http reply * /health** 200
http proxypass * /**
httpsrv -p{srv}
""", ports=[srv])
url = f"http://127.0.0.1:{srv}"
t.contains(t.http(url + "/local/a.html"), "<h1>local</h1>",
"a rule of its own is still answered here")
t.eq(200, t.http(url + "/health").status, "and so is another")
r = t.http(f"http://127.0.0.1:{one}/echo", proxy=f"127.0.0.1:{srv}")
t.eq(200, r.status, "a request the rules do not answer is proxied")
t.contains(r, "path=/echo", "and the origin sees it")
# a client which sends an origin-form request with a Host header reaches
# the same place: what decides is which rule matches, not the form
r = t.http(url + "/echo", headers={"Host": f"127.0.0.1:{one}"})
t.contains(r, "path=/echo", "an origin-form request is proxied the same way")
# --- an access rule which sends the rest to the proxy -------------------
# allow, with a chain to the local proxy, then a second rule for the pass
# the proxy itself makes
rsrv = t.free_port()
t.start("httpsrv_proxypass_acl", f"""
log
auth iponly
allow *
parent 1000 http 0.0.0.0 0
allow *
http file * /local/*.html "{root}/$1.html"
httpsrv -p{rsrv}
""", ports=[rsrv])
rurl = f"http://127.0.0.1:{rsrv}"
t.contains(t.http(rurl + "/local/a.html"), "<h1>local</h1>",
"a rule still wins over the redirect")
r = t.http(f"http://127.0.0.1:{one}/echo", proxy=f"127.0.0.1:{rsrv}")
t.eq(200, r.status, "and what no rule matches goes to the proxy the rule named")
# --- rules after the chain decide what the proxy may fetch -------------
# The service answers for itself on the first pass, so an address or a
# port there is the one the client connected to; on the pass the proxy
# makes, it is the one the request names.
gsrv = t.free_port()
t.start("httpsrv_proxypass_gate", f"""
log
auth iponly
allow *
parent 1000 http 0.0.0.0 0
allow * * 127.0.0.1/32 {one}
deny *
httpsrv -p{gsrv}
""", ports=[gsrv])
t.eq(200, t.http(f"http://127.0.0.1:{one}/echo", proxy=f"127.0.0.1:{gsrv}").status,
"a destination a later rule allows is fetched")
t.eq(403, t.http(f"http://127.0.0.1:{two}/echo", proxy=f"127.0.0.1:{gsrv}").status,
"and one no rule allows is refused")
# a deny written before the rule carrying the chain applies as well
bsrv = t.free_port()
t.start("httpsrv_proxypass_deny", f"""
log
auth iponly
deny * * 127.0.0.1/32 {two}
allow *
parent 1000 http 0.0.0.0 0
allow *
httpsrv -p{bsrv}
""", ports=[bsrv])
t.eq(200, t.http(f"http://127.0.0.1:{one}/echo", proxy=f"127.0.0.1:{bsrv}").status,
"what the deny does not name is still fetched")
t.eq(403, t.http(f"http://127.0.0.1:{two}/echo", proxy=f"127.0.0.1:{bsrv}").status,
"a deny before the chain stops the request too")
# --- one connection, both kinds of request -----------------------------
text, closed = t.raw_session(srv,
f"GET /local/a.html HTTP/1.1\r\nHost: t\r\n\r\n"
f"GET http://127.0.0.1:{one}/echo HTTP/1.1\r\nHost: 127.0.0.1:{one}\r\n\r\n"
f"GET http://127.0.0.1:{two}/echo HTTP/1.1\r\nHost: 127.0.0.1:{two}\r\n\r\n"
f"GET /local/a.html HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n",
quiet=2)
t.eq(4, text.count("HTTP/1."), "four requests are answered on one connection")
t.eq(2, text.count("<h1>local</h1>"), "two of them here")
t.eq(2, text.count("peer.addr="), "and two by the origins")
t.eq(True, closed, "the last one ends it")
# --- every kind of rule on the same connection --------------------------
with open(os.path.join(root, "f.html"), "w") as fp:
fp.write("FILEBODY")
with open(os.path.join(root, "c.html"), "w") as fp:
fp.write("CACHEBODY")
msrv = t.free_port()
t.start("httpsrv_proxypass_mix", f"""
log
auth iponly
allow *
http file * /f/*.html "{root}/$1.html"
http cache * /c/*.html "{root}/$1.html"
http proxypass * /**
httpsrv -p{msrv}
""", ports=[msrv])
proxied = f"GET http://127.0.0.1:{one}/echo HTTP/1.1\r\nHost: 127.0.0.1:{one}\r\n\r\n"
text, closed = t.raw_session(msrv,
"GET /f/f.html HTTP/1.1\r\nHost: t\r\n\r\n"
"GET /c/c.html HTTP/1.1\r\nHost: t\r\n\r\n"
+ proxied +
"GET /c/c.html HTTP/1.1\r\nHost: t\r\n\r\n"
+ proxied +
"GET /f/f.html HTTP/1.1\r\nHost: t\r\nConnection: close\r\n\r\n",
quiet=2)
t.eq(6, text.count("HTTP/1."), "file, cache and proxypass share one connection")
t.eq(2, text.count("FILEBODY"), "both files arrive")
t.eq(2, text.count("CACHEBODY"), "both cached files arrive")
t.eq(2, text.count("peer.addr="), "and both proxied requests arrive")
t.eq(True, closed, "the request asking to close ends it")
# --- a proxied answer of unstated length ends the connection ------------
# Its body is delimited by the close, so nothing can follow it here
# either: the client has to ask again on a new connection.
closer = t.free_port()
stop = t.raw_server(closer,
b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\nCLOSEDELIMITED",
close_after=True)
try:
text, closed = t.raw_session(msrv,
f"GET http://127.0.0.1:{closer}/x HTTP/1.1\r\nHost: 127.0.0.1:{closer}\r\n\r\n"
"GET /f/f.html HTTP/1.1\r\nHost: t\r\n\r\n", quiet=2)
t.eq(1, text.count("HTTP/1."), "the answer of unstated length is the last one")
t.contains(text, "CLOSEDELIMITED", "and its body still arrives whole")
t.eq(True, closed, "the connection ends with it")
finally:
stop()
# --- credentials go where a proxy expects them --------------------------
asrv = t.free_port()
t.start("httpsrv_proxypass_auth", f"""
log
users u:CL:p
auth strong
allow u
http file * /local/*.html "{root}/$1.html"
http proxypass * /**
httpsrv -p{asrv}
""", ports=[asrv])
aurl = f"http://127.0.0.1:{asrv}"
r = t.http(f"http://127.0.0.1:{one}/echo", proxy=f"127.0.0.1:{asrv}")
t.eq(407, r.status, "a proxy-style request with no credentials is asked for them")
t.contains(r.header("Proxy-Authenticate") or "", "Basic",
"with the header a proxy client reads")
r = t.http(f"http://127.0.0.1:{one}/echo", proxy=f"127.0.0.1:{asrv}",
proxy_auth=("u", "p"))
t.eq(200, r.status, "and is served once they are given")
r = t.http(aurl + "/local/a.html")
t.eq(401, r.status, "a request to the site itself is asked the site's way")
t.contains(r.header("WWW-Authenticate") or "", "Basic", "with its own header")
t.contains(t.http(aurl + "/local/a.html", auth=("u", "p")), "<h1>local</h1>",
"and answered once they are given")

View File

@ -123,6 +123,7 @@ class Tester:
self.servers = [] self.servers = []
self.checks = [] self.checks = []
self.timeout = 10 self.timeout = 10
self._raw_kept = []
self._skipped = 0 self._skipped = 0
self._certs = None self._certs = None
self.logs = [] self.logs = []
@ -357,6 +358,48 @@ class Tester:
return f"<no reply: {exc}>", True return f"<no reply: {exc}>", True
return b"".join(chunks).decode("utf-8", "replace"), closed return b"".join(chunks).decode("utf-8", "replace"), closed
def raw_server(self, port, reply, close_after=True, host="127.0.0.1"):
"""Answer every connection with fixed bytes. Returns a stop function.
For the shapes a real server would have to be talked into: an answer
whose body is delimited by the close, or one which promises to stay
and does not.
"""
sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((host, port))
sock.listen(8)
running = [True]
def serve():
while running[0]:
try:
conn, _ = sock.accept()
except OSError:
break
try:
conn.settimeout(self.timeout)
conn.recv(65536)
conn.sendall(reply)
if close_after:
conn.close()
else:
self._raw_kept.append(conn)
except OSError:
pass
thread = threading.Thread(target=serve, daemon=True)
thread.start()
def stop():
running[0] = False
try:
sock.close()
except OSError:
pass
return stop
# ---- UDP --------------------------------------------------------- # ---- UDP ---------------------------------------------------------
def udp_echo(self, prefix=b"echo:"): def udp_echo(self, prefix=b"echo:"):