Skip to content

Commit 42259ac

Browse files
committed
mod_proxy_fcgi: Handle CGI Local Redirect Responses
* modules/proxy/mod_proxy_fcgi.c (dispatch): Recognize a CGI Local Redirect Response (RFC 3875, 6.2.2), a Location with a local URI path and no other header, read the rest of the response and remember the URI. (fcgi_request_status): New, does the internal redirect once the connection to the backend has been released, like mod_proxy_scgi. * test/modules/proxy/fcgi_faker.py, test_07_fcgi_local_redirect.py: New. * test/modules/proxy/env.py: Load proxy_fcgi. * docs/log-message-tags/next-number: Update. PR: 61430
1 parent 3e2a882 commit 42259ac

6 files changed

Lines changed: 406 additions & 5 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*) mod_proxy_fcgi: Handle CGI Local Redirect Responses according to
2+
RFC 3875 section 6.2.2. PR 61430.
3+
[Arturo Bernal]

‎docs/log-message-tags/next-number‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
10635
1+
10636

‎modules/proxy/mod_proxy_fcgi.c‎

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ typedef struct {
2929

3030
typedef struct {
3131
char *dirwalk_uri_path;
32+
const char *local_redirect; /* URI path of a CGI local redirect response */
3233
} fcgi_req_config_t;
3334

3435
/* We will assume FPM, but still differentiate */
@@ -593,6 +594,48 @@ static int handle_headers(request_rec *r, int *state,
593594
return 0;
594595
}
595596

597+
/* What the CGI response headers did to the request, see local_redirect(). */
598+
typedef struct {
599+
int nheaders;
600+
int nerr_headers;
601+
const char *content_type;
602+
const char *status_line;
603+
} resp_snapshot;
604+
605+
static void snapshot_response(request_rec *r, resp_snapshot *snap)
606+
{
607+
snap->nheaders = apr_table_elts(r->headers_out)->nelts;
608+
snap->nerr_headers = apr_table_elts(r->err_headers_out)->nelts;
609+
snap->content_type = r->content_type;
610+
snap->status_line = r->status_line;
611+
}
612+
613+
/*
614+
* Returns the local URI path of a CGI Local Redirect Response (RFC 3875,
615+
* 6.2.2), which is a response without a status or a content type and with
616+
* nothing but a Location of a URI path in its headers.
617+
*/
618+
static const char *local_redirect(request_rec *r, const resp_snapshot *before)
619+
{
620+
resp_snapshot now;
621+
const char *location;
622+
623+
snapshot_response(r, &now);
624+
if (r->status != HTTP_OK
625+
|| now.status_line != before->status_line
626+
|| now.content_type != before->content_type
627+
|| now.nheaders != before->nheaders + 1
628+
|| now.nerr_headers != before->nerr_headers) {
629+
return NULL;
630+
}
631+
632+
location = apr_table_get(r->headers_out, "Location");
633+
if (location && location[0] == '/' && location[1] != '/') {
634+
return location;
635+
}
636+
return NULL;
637+
}
638+
596639
static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf,
597640
request_rec *r, apr_pool_t *setaside_pool,
598641
apr_uint16_t request_id, const char **err,
@@ -601,6 +644,7 @@ static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf,
601644
{
602645
apr_bucket_brigade *ib, *ob;
603646
int seen_end_of_headers = 0, done = 0, ignore_body = 0;
647+
int redirected = 0;
604648
apr_status_t rv = APR_SUCCESS;
605649
int script_error_status = HTTP_OK;
606650
conn_rec *c = r->connection;
@@ -828,11 +872,35 @@ static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf,
828872

829873
if (st == 1) {
830874
int status;
875+
const char *location;
876+
resp_snapshot before;
831877
seen_end_of_headers = 1;
832878

879+
snapshot_response(r, &before);
833880
status = ap_scan_script_header_err_brigade_ex(r, ob,
834881
NULL, APLOG_MODULE_INDEX);
835882

883+
/* A local redirect is done by fcgi_request_status()
884+
* once the response is complete and the connection
885+
* to the backend is released, so read and drop the
886+
* rest of it like the body of a 304. */
887+
if (status == OK
888+
&& (location = local_redirect(r, &before))) {
889+
fcgi_req_config_t *rconf =
890+
ap_get_module_config(r->request_config,
891+
&proxy_fcgi_module);
892+
893+
if (rconf == NULL) {
894+
rconf = apr_pcalloc(r->pool, sizeof(*rconf));
895+
ap_set_module_config(r->request_config,
896+
&proxy_fcgi_module,
897+
rconf);
898+
}
899+
rconf->local_redirect = location;
900+
apr_table_unset(r->headers_out, "Location");
901+
redirected = ignore_body = 1;
902+
}
903+
836904
/* FCGI has its own body framing mechanism which we don't
837905
* match against any provided Content-Length, so let the
838906
* core determine C-L vs T-E based on what's actually sent.
@@ -942,7 +1010,7 @@ static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf,
9421010
} else {
9431011
/* XXX what if we haven't seen end of the headers yet? */
9441012

945-
if (script_error_status == HTTP_OK) {
1013+
if (script_error_status == HTTP_OK && !redirected) {
9461014
b = apr_bucket_eos_create(c->bucket_alloc);
9471015
APR_BRIGADE_INSERT_TAIL(ob, b);
9481016

@@ -1384,10 +1452,42 @@ static const char *cmd_setenv(cmd_parms *cmd, void *in_dconf,
13841452

13851453
return NULL;
13861454
}
1455+
1456+
/*
1457+
* Does the local redirect found by dispatch() once the request to the backend
1458+
* is complete, on the request_status hook.
1459+
*/
1460+
static int fcgi_request_status(int *status, request_rec *r)
1461+
{
1462+
fcgi_req_config_t *rconf = ap_get_module_config(r->request_config,
1463+
&proxy_fcgi_module);
1464+
1465+
if (*status == OK && rconf && rconf->local_redirect) {
1466+
const char *location = rconf->local_redirect;
1467+
1468+
ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r, APLOGNO(10635)
1469+
"Internal redirect to %s", location);
1470+
1471+
r->status_line = NULL;
1472+
if (r->method_number != M_GET) {
1473+
/* keep HEAD, which is passed around as M_GET, too */
1474+
r->method = "GET";
1475+
r->method_number = M_GET;
1476+
}
1477+
apr_table_unset(r->headers_in, "Content-Length");
1478+
ap_internal_redirect_handler(location, r);
1479+
return OK;
1480+
}
1481+
1482+
return DECLINED;
1483+
}
1484+
13871485
static void register_hooks(apr_pool_t *p)
13881486
{
13891487
proxy_hook_scheme_handler(proxy_fcgi_handler, NULL, NULL, APR_HOOK_FIRST);
13901488
proxy_hook_canon_handler(proxy_fcgi_canon, NULL, NULL, APR_HOOK_FIRST);
1489+
APR_OPTIONAL_HOOK(proxy, request_status, fcgi_request_status, NULL, NULL,
1490+
APR_HOOK_MIDDLE);
13911491
}
13921492

13931493
static const command_rec command_table[] = {

‎test/modules/proxy/env.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ def __init__(self, env: 'HttpdTestEnv'):
7171
super().__init__(env=env)
7272
self.add_source_dir(os.path.dirname(inspect.getfile(ProxyTestSetup)))
7373
self.add_modules(["proxy", "proxy_http", "proxy_ajp", "proxy_balancer",
74-
"proxy_connect", "proxy_uwsgi", "lbmethod_byrequests",
74+
"proxy_connect", "proxy_uwsgi", "proxy_fcgi",
75+
"lbmethod_byrequests",
7576
"remoteip"])
7677

7778

@@ -86,8 +87,8 @@ def __init__(self, pytestconfig=None):
8687
self._d_mixed = f"mixed.{self.http_tld}"
8788

8889
self.add_httpd_log_modules(
89-
["proxy", "proxy_http", "proxy_balancer", "lbmethod_byrequests",
90-
"ssl"])
90+
["proxy", "proxy_http", "proxy_fcgi", "proxy_balancer",
91+
"lbmethod_byrequests", "ssl"])
9192
self.add_cert_specs([
9293
CertificateSpec(domains=[
9394
self._d_forward, self._d_reverse, self._d_mixed

‎test/modules/proxy/fcgi_faker.py‎

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""
2+
Minimal FastCGI Responder for testing mod_proxy_fcgi.
3+
4+
Listens on a TCP port and answers every request with the CGI response which
5+
the test registered for its SCRIPT_NAME. A connection is kept open for as
6+
many requests as the client sends on it, so that the tests can tell whether
7+
mod_proxy_fcgi reused a connection and whether it left it in a usable state.
8+
"""
9+
import socket
10+
import struct
11+
import threading
12+
13+
FCGI_BEGIN_REQUEST = 1
14+
FCGI_END_REQUEST = 3
15+
FCGI_PARAMS = 4
16+
FCGI_STDIN = 5
17+
FCGI_STDOUT = 6
18+
19+
MAX_CONTENT = 65535
20+
21+
22+
def _read_exact(sock, n):
23+
data = b""
24+
while len(data) < n:
25+
chunk = sock.recv(n - len(data))
26+
if not chunk:
27+
return None
28+
data += chunk
29+
return data
30+
31+
32+
def _parse_params(data):
33+
params = {}
34+
pos = 0
35+
36+
def length():
37+
nonlocal pos
38+
if data[pos] >> 7:
39+
n = struct.unpack_from(">I", data, pos)[0] & 0x7fffffff
40+
pos += 4
41+
else:
42+
n = data[pos]
43+
pos += 1
44+
return n
45+
46+
while pos < len(data):
47+
nlen = length()
48+
vlen = length()
49+
name = data[pos:pos + nlen].decode("latin-1")
50+
pos += nlen
51+
params[name] = data[pos:pos + vlen].decode("latin-1")
52+
pos += vlen
53+
return params
54+
55+
56+
def _record(rtype, request_id, content=b""):
57+
return struct.pack(">BBHHBx", 1, rtype, request_id, len(content), 0) + content
58+
59+
60+
class FcgiFaker:
61+
62+
def __init__(self, host, port):
63+
self._host = host
64+
self._port = port
65+
self._socket = None
66+
self._thread = None
67+
self._done = False
68+
self._lock = threading.Lock()
69+
self._conn_count = 0
70+
# path -> exact CGI response (headers and body) sent over FCGI_STDOUT
71+
self.routes = {}
72+
# what the backend has seen, in order: one dict per request
73+
self.requests = []
74+
75+
def start(self):
76+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
77+
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
78+
self._socket.bind((self._host, self._port))
79+
self._socket.listen(8)
80+
self._socket.settimeout(0.5)
81+
self._thread = threading.Thread(target=self._accept, daemon=True)
82+
self._thread.start()
83+
84+
def stop(self):
85+
self._done = True
86+
self._thread.join(timeout=5)
87+
self._socket.close()
88+
89+
def reset(self):
90+
with self._lock:
91+
self.requests = []
92+
93+
def _accept(self):
94+
while not self._done:
95+
try:
96+
conn, _ = self._socket.accept()
97+
except socket.timeout:
98+
continue
99+
except OSError:
100+
return
101+
with self._lock:
102+
self._conn_count += 1
103+
conn_id = self._conn_count
104+
threading.Thread(target=self._serve, args=(conn, conn_id),
105+
daemon=True).start()
106+
107+
def _serve(self, conn, conn_id):
108+
conn.settimeout(30)
109+
request_id = 0
110+
params = b""
111+
stdin = b""
112+
try:
113+
while True:
114+
header = _read_exact(conn, 8)
115+
if header is None:
116+
return
117+
_, rtype, request_id_, clen, plen = struct.unpack(">BBHHBx", header)
118+
content = _read_exact(conn, clen) if clen else b""
119+
if plen:
120+
_read_exact(conn, plen)
121+
if rtype == FCGI_BEGIN_REQUEST:
122+
request_id = request_id_
123+
params = b""
124+
stdin = b""
125+
elif rtype == FCGI_PARAMS:
126+
params += content
127+
elif rtype == FCGI_STDIN:
128+
stdin += content
129+
if clen == 0:
130+
self._respond(conn, conn_id, request_id,
131+
_parse_params(params), stdin)
132+
except OSError:
133+
return
134+
finally:
135+
conn.close()
136+
137+
def _respond(self, conn, conn_id, request_id, params, stdin):
138+
# SCRIPT_NAME is the path of the request, REQUEST_URI stays the one
139+
# the client sent when the server redirects internally.
140+
path = params.get("SCRIPT_NAME", "")
141+
with self._lock:
142+
self.requests.append({"conn": conn_id, "uri": path,
143+
"params": params, "stdin": stdin})
144+
if path in self.routes:
145+
out = self.routes[path]
146+
if callable(out):
147+
out = out(params, stdin)
148+
else:
149+
out = b"Status: 404 Not Found\r\nContent-Type: text/plain\r\n\r\nno route\n"
150+
for i in range(0, len(out), MAX_CONTENT):
151+
conn.sendall(_record(FCGI_STDOUT, request_id, out[i:i + MAX_CONTENT]))
152+
conn.sendall(_record(FCGI_STDOUT, request_id))
153+
conn.sendall(_record(FCGI_END_REQUEST, request_id,
154+
struct.pack(">IBxxx", 0, 0)))

0 commit comments

Comments
 (0)