diff --git a/CMakeLists.txt b/CMakeLists.txt index a567b9d..fed8642 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -169,7 +169,7 @@ if(WIN32) endif() # Windows libraries - set(WINDOWS_LIBS ws2_32 advapi32 user32 kernel32 gdi32 crypt32) + set(WINDOWS_LIBS ws2_32 mswsock advapi32 user32 kernel32 gdi32 crypt32) # Windows plugins (always built) set(DEFAULT_PLUGINS diff --git a/Makefile.msvc b/Makefile.msvc index cae7ce5..0e4bbb5 100644 --- a/Makefile.msvc +++ b/Makefile.msvc @@ -24,7 +24,7 @@ LN = link LDFLAGS = /nologo /subsystem:console /incremental:no DLFLAGS = /DLL DLSUFFICS = .dll -LIBS = ws2_32.lib advapi32.lib odbc32.lib user32.lib kernel32.lib Gdi32.lib Crypt32.lib $(SSL_LIBS) pcre2-8.lib +LIBS = ws2_32.lib mswsock.lib advapi32.lib odbc32.lib user32.lib kernel32.lib Gdi32.lib Crypt32.lib $(SSL_LIBS) pcre2-8.lib LIBSPREFIX = LIBSSUFFIX = .lib LIBEXT = .lib diff --git a/Makefile.watcom b/Makefile.watcom index fe9faaa..00df629 100644 --- a/Makefile.watcom +++ b/Makefile.watcom @@ -14,7 +14,7 @@ LN = link LDFLAGS = /nologo /subsystem:console /incremental:no DLFLAGS = /DLL DLSUFFICS = .dll -LIBS = ws2_32.lib advapi32.lib user32.lib kernel32.lib +LIBS = ws2_32.lib mswsock.lib advapi32.lib user32.lib kernel32.lib LIBSPREFIX = LIBSSUFFIX = .lib LIBEXT = .lib diff --git a/Makefile.win b/Makefile.win index 065cb77..9d7a2dd 100644 --- a/Makefile.win +++ b/Makefile.win @@ -30,7 +30,7 @@ ifeq ($(HTTPSRV),true) endif DLFLAGS ?= -shared DLSUFFICS = .dll -LIBS += -lws2_32 -lodbc32 -ladvapi32 -luser32 -lbcrypt +LIBS += -lws2_32 -lmswsock -lodbc32 -ladvapi32 -luser32 -lbcrypt LIBSPREFIX = -l LIBSSUFFIX = LNOUT = -o diff --git a/doc/html/howtoe.html b/doc/html/howtoe.html index 0ec3ac7..0975f2b 100644 --- a/doc/html/howtoe.html +++ b/doc/html/howtoe.html @@ -867,6 +867,65 @@ http file * /** /usr/local/web/blocked.html text/html * * 403 httpsrv -p8080 -i127.0.0.1

+Both a site and a proxy. A request may arrive the way it arrives at a +site - a path, with the name in the Host header - or the way it arrives at a +proxy, naming the whole URL, or the host alone with CONNECT. Both are read. A +proxy-form request authenticates with Proxy-Authorization and is refused with +407, the way a proxy refuses one; a site-form request uses Authorization and +401. What answers it is decided by the rules either way. +

+

+proxypass is the rule which answers by fetching, so one service can +serve what it has and proxy the rest: +

+auth iponly
+allow *
+http file * /local/** "/usr/local/web/$1"
+http proxypass * /**
+httpsrv -p8080
+
+

+An access rule redirecting to the local proxy does the same without a rule for +it. The chain with no address is what "the local proxy" is written as, and the +second allow is what the proxy matches on the pass it makes itself, +since a rule carrying the chain is not taken twice: +

+auth iponly
+allow *
+parent 1000 http 0.0.0.0 0
+allow *
+http file * /local/** "/usr/local/web/$1"
+httpsrv -p8080
+
+

+Authentication happens twice for the same reason - once for the service, once +for the proxy - so a configuration which asks for credentials asks for them the +way a proxy does. +

+

+The access rules are read from the top on both passes, and the second one is +where the request's destination is known. On the first pass the service is +answering for itself, so an address or a port in a rule is matched against the +address the client connected to; the name from the request is matched on both +passes. On the second the destination is the one the request names, so rules +written with an address, a port or a name decide what the proxy may fetch, and +they decide it before it connects: +

+auth iponly
+allow *
+parent 1000 http 0.0.0.0 0
+allow * * * 80,443
+deny *
+httpsrv -p8080
+
+

+Everything reaches the rules, only ports 80 and 443 are fetched, and a +deny written before the rule carrying the chain applies on both +passes just the same. The connection to the server is kept for the next request and +closed when that request goes elsewhere, or when the server has closed it in the +meantime. +

+

Connections. A client asking in HTTP/1.1 gets a 1.1 answer and the connection is kept for the next request, unless it sent Connection: close; a 1.0 client has to ask for keep-alive. The diff --git a/doc/html/howtor.html b/doc/html/howtor.html index 21aab93..723d299 100644 --- a/doc/html/howtor.html +++ b/doc/html/howtor.html @@ -877,6 +877,63 @@ http file * /** /usr/local/web/blocked.html text/html * * 403 httpsrv -p8080 -i127.0.0.1

+И сайт, и прокси. Запрос может прийти так, как приходит на сайт - путь, +имя в заголовке Host, - или так, как приходит на прокси: с полным URL, либо, для +туннеля, с одним именем хоста в CONNECT. Читается и то, и другое. Запрос в форме +для прокси аутентифицируется через Proxy-Authorization и отклоняется кодом 407, +как это делает прокси; запрос в форме для сайта - через Authorization и 401. Чем +он будет обработан, в обоих случаях решают правила. +

+

+proxypass - правило, которое отвечает, забирая ресурс, поэтому один +сервис может отдавать своё и проксировать остальное: +

+auth iponly
+allow *
+http file * /local/** "/usr/local/web/$1"
+http proxypass * /**
+httpsrv -p8080
+
+

+Правило доступа с перенаправлением на локальный прокси делает то же самое без +отдельного правила. Цепочка без адреса и означает "локальный прокси", а второй +allow - то, с чем совпадает сам прокси на своём проходе, так как +правило с цепочкой второй раз не берётся: +

+auth iponly
+allow *
+parent 1000 http 0.0.0.0 0
+allow *
+http file * /local/** "/usr/local/web/$1"
+httpsrv -p8080
+
+

+Аутентификация по той же причине происходит дважды - для сервиса и для прокси, - +поэтому конфигурация, требующая учётных данных, запрашивает их так, как это +делает прокси. +

+

+Правила доступа просматриваются с начала на обоих проходах, и назначение запроса +известно на втором. На первом сервис отвечает сам за себя, поэтому адрес или порт +в правиле сопоставляется с адресом, на который подключился клиент; имя из запроса +сопоставляется на обоих проходах. На втором назначение - то, которое названо в +запросе, поэтому правила с адресом, портом или именем определяют, что прокси +разрешено забрать, и определяют это до установления соединения: +

+auth iponly
+allow *
+parent 1000 http 0.0.0.0 0
+allow * * * 80,443
+deny *
+httpsrv -p8080
+
+

+До правил доходит всё, забираются только порты 80 и 443, а deny, +записанный до правила с цепочкой, действует на обоих проходах точно так же. Соединение с сервером сохраняется для следующего запроса и +закрывается, если следующий запрос идёт в другое место или если сервер за это +время его закрыл. +

+

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

local

") + + # 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"), "

local

", + "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"), "

local

", + "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("

local

"), "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")), "

local

", + "and answered once they are given") diff --git a/tests/harness.py b/tests/harness.py index 11fce96..b275c60 100644 --- a/tests/harness.py +++ b/tests/harness.py @@ -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"", 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:"):