mirror of
https://github.com/3proxy/3proxy.git
synced 2026-09-02 21:05:49 +08:00
support proxypass in web server
This commit is contained in:
parent
8971fcf991
commit
ee0de3613a
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 явно.
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
240
src/httpsrv.c
240
src/httpsrv.c
@ -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(¶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
|
/* 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
|
||||||
|
|||||||
10
src/proxy.c
10
src/proxy.c
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
203
tests/cases/httpsrv_proxypass.py
Normal file
203
tests/cases/httpsrv_proxypass.py
Normal 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")
|
||||||
@ -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:"):
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user