mirror of
https://github.com/3proxy/3proxy.git
synced 2026-09-02 12:55:49 +08:00
support proxypass in web server
This commit is contained in:
parent
8971fcf991
commit
ee0de3613a
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -867,6 +867,65 @@ http file * /** /usr/local/web/blocked.html text/html * * 403
|
||||
httpsrv -p8080 -i127.0.0.1
|
||||
</pre>
|
||||
<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
|
||||
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
|
||||
|
||||
@ -877,6 +877,63 @@ http file * /** /usr/local/web/blocked.html text/html * * 403
|
||||
httpsrv -p8080 -i127.0.0.1
|
||||
</pre>
|
||||
<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, и
|
||||
соединение сохраняется для следующего запроса, если он не прислал
|
||||
<code>Connection: close</code>; клиенту 1.0 нужно запросить keep-alive явно.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
240
src/httpsrv.c
240
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
|
||||
|
||||
10
src/proxy.c
10
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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
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.checks = []
|
||||
self.timeout = 10
|
||||
self._raw_kept = []
|
||||
self._skipped = 0
|
||||
self._certs = None
|
||||
self.logs = []
|
||||
@ -357,6 +358,48 @@ class Tester:
|
||||
return f"<no reply: {exc}>", True
|
||||
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 ---------------------------------------------------------
|
||||
|
||||
def udp_echo(self, prefix=b"echo:"):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user