diff --git a/changes-entries/64919.txt b/changes-entries/64919.txt
new file mode 100644
index 00000000000..d5cef9cbb73
--- /dev/null
+++ b/changes-entries/64919.txt
@@ -0,0 +1,3 @@
+ *) mod_proxy_fcgi: Honor the responsefieldsize parameter of the worker for
+ the size of the response header lines, so that lines larger than the
+ default of 8192 bytes are supported. PR 64919. [Arturo Bernal]
diff --git a/docs/manual/mod/mod_proxy.xml b/docs/manual/mod/mod_proxy.xml
index 32285802afd..70070536cd1 100644
--- a/docs/manual/mod/mod_proxy.xml
+++ b/docs/manual/mod/mod_proxy.xml
@@ -1220,6 +1220,8 @@ ProxyPass "/example" "http://backend.example.com" max=20 ttl=120 retry=300
a proxied response. Setting the value to 0 will use the system
default of 8192 bytes.
Available in Apache HTTP Server 2.4.34 and later.
+ mod_proxy_fcgi respects it as well, in 2.5.1 and
+ later.
| keepalive |
Off |
diff --git a/include/ap_mmn.h b/include/ap_mmn.h
index b69d07b74af..03d62666a09 100644
--- a/include/ap_mmn.h
+++ b/include/ap_mmn.h
@@ -739,6 +739,8 @@
* 20211221.31 (2.5.1-dev) Add ap_*_timingsafe() to httpd.h
* 20211221.32 (2.5.1-dev) Add ap_cstr_stripws() to httpd.h
* 20211221.33 (2.5.1-dev) Add ap_size_parameter_parse() to httpd.h
+ * 20211221.34 (2.5.1-dev) Add ap_scan_script_header_err_brigade_limit() to
+ * util_script.h
*/
#define MODULE_MAGIC_COOKIE 0x41503235UL /* "AP25" */
@@ -746,7 +748,7 @@
#ifndef MODULE_MAGIC_NUMBER_MAJOR
#define MODULE_MAGIC_NUMBER_MAJOR 20211221
#endif
-#define MODULE_MAGIC_NUMBER_MINOR 33 /* 0...n */
+#define MODULE_MAGIC_NUMBER_MINOR 34 /* 0...n */
/**
* Determine if the server's current MODULE_MAGIC_NUMBER is at least a
diff --git a/include/util_script.h b/include/util_script.h
index 95f238d53bd..cf3138ae29e 100644
--- a/include/util_script.h
+++ b/include/util_script.h
@@ -156,6 +156,31 @@ AP_DECLARE(int) ap_scan_script_header_err_brigade_ex(request_rec *r,
char *buffer,
int module_index);
+/**
+ * Read headers output from a script like
+ * ap_scan_script_header_err_brigade_ex(), with a limit on the size of the
+ * header lines. A header line is read into a buffer of line_size bytes, so it
+ * can be at most line_size - 3 bytes long, without its line break.
+ * @param r The current request
+ * @param bb The brigade from which to read
+ * @param buffer Empty when calling the function. On output, if there was an
+ * error, the string that cause the error is stored here. If it
+ * is not NULL, it must have room for line_size bytes.
+ * @param module_index The module index to be used for logging
+ * @param line_size The size of the buffer to read a header line into.
+ * MAX_STRING_LEN is what
+ * ap_scan_script_header_err_brigade_ex() uses.
+ * @return HTTP_OK on success, HTTP_INTERNAL_SERVER_ERROR or other 5xx status
+ * code on failure, or a conditional status code (HTTP_NOT_MODIFIED or
+ * HTTP_PRECONDITION_FAILED) to indicate that the script's response does
+ * not meet the request's conditions
+ */
+AP_DECLARE(int) ap_scan_script_header_err_brigade_limit(request_rec *r,
+ apr_bucket_brigade *bb,
+ char *buffer,
+ int module_index,
+ apr_size_t line_size);
+
/**
* Read headers strings from a script, ensuring that the output is valid. If
* the output is valid, then the headers are added to the headers out of the
diff --git a/modules/proxy/mod_proxy_fcgi.c b/modules/proxy/mod_proxy_fcgi.c
index f9b28dc7889..d694990335a 100644
--- a/modules/proxy/mod_proxy_fcgi.c
+++ b/modules/proxy/mod_proxy_fcgi.c
@@ -614,8 +614,12 @@ static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf,
char stack_iobuf[AP_IOBUFSIZE];
apr_size_t iobuf_size = AP_IOBUFSIZE;
char *iobuf = stack_iobuf;
+ apr_size_t header_line_size = HUGE_STRING_LEN;
*err = NULL;
+ if (conn->worker->s->response_field_size_set) {
+ header_line_size = conn->worker->s->response_field_size;
+ }
if (conn->worker->s->io_buffer_size_set) {
iobuf_size = conn->worker->s->io_buffer_size;
/* Allocate a buffer if the configured size is larger than the
@@ -830,8 +834,8 @@ static apr_status_t dispatch(proxy_conn_rec *conn, proxy_dir_conf *conf,
int status;
seen_end_of_headers = 1;
- status = ap_scan_script_header_err_brigade_ex(r, ob,
- NULL, APLOG_MODULE_INDEX);
+ status = ap_scan_script_header_err_brigade_limit(r, ob,
+ NULL, APLOG_MODULE_INDEX, header_line_size);
/* FCGI has its own body framing mechanism which we don't
* match against any provided Content-Length, so let the
diff --git a/server/util_script.c b/server/util_script.c
index 5c3f41996b1..a7cbbccb4c4 100644
--- a/server/util_script.c
+++ b/server/util_script.c
@@ -25,6 +25,8 @@
#include
#endif
+#include /* for INT_MAX */
+
#include "ap_config.h"
#include "httpd.h"
#include "http_config.h"
@@ -479,13 +481,16 @@ static int set_cookie_doo_doo(void *v, const char *key, const char *val)
#define HTTP_UNSET (-HTTP_OK)
#define SCRIPT_LOG_MARK __FILE__,__LINE__,module_index
-AP_DECLARE(int) ap_scan_script_header_err_core_ex(request_rec *r, char *buffer,
- int (*getsfunc) (char *, int, void *),
- void *getsfunc_data,
- int module_index)
+/* Scan the headers read by getsfunc using lines of up to bufsize bytes,
+ * the buffer being the caller's if there is one. */
+static int scan_script_header_err(request_rec *r, char *buffer,
+ apr_size_t bufsize,
+ int (*getsfunc) (char *, int, void *),
+ void *getsfunc_data, int module_index)
{
char x[MAX_STRING_LEN];
char *w, *l;
+ int len;
apr_size_t p;
int cgi_status = HTTP_UNSET;
apr_table_t *merge;
@@ -496,7 +501,19 @@ AP_DECLARE(int) ap_scan_script_header_err_core_ex(request_rec *r, char *buffer,
if (buffer) {
*buffer = '\0';
}
- w = buffer ? buffer : x;
+ if (bufsize > INT_MAX) {
+ bufsize = INT_MAX;
+ }
+ if (buffer) {
+ w = buffer;
+ }
+ else if (bufsize > sizeof(x)) {
+ w = apr_palloc(r->pool, bufsize);
+ }
+ else {
+ w = x;
+ }
+ len = bufsize > 1 ? (int)bufsize - 1 : 1;
/* temporary place to hold headers to merge in later */
merge = apr_table_make(r->pool, 10);
@@ -512,7 +529,7 @@ AP_DECLARE(int) ap_scan_script_header_err_core_ex(request_rec *r, char *buffer,
while (1) {
- int rv = (*getsfunc) (w, MAX_STRING_LEN - 1, getsfunc_data);
+ int rv = (*getsfunc) (w, len, getsfunc_data);
if (rv == 0) {
const char *msg = "Premature end of script headers";
if (first_header)
@@ -627,7 +644,7 @@ AP_DECLARE(int) ap_scan_script_header_err_core_ex(request_rec *r, char *buffer,
if (!(l = strchr(w, ':'))) {
if (!buffer) {
/* Soak up all the script output - may save an outright kill */
- while ((*getsfunc)(w, MAX_STRING_LEN - 1, getsfunc_data) > 0) {
+ while ((*getsfunc)(w, len, getsfunc_data) > 0) {
continue;
}
}
@@ -751,6 +768,15 @@ AP_DECLARE(int) ap_scan_script_header_err_core_ex(request_rec *r, char *buffer,
return OK;
}
+AP_DECLARE(int) ap_scan_script_header_err_core_ex(request_rec *r, char *buffer,
+ int (*getsfunc) (char *, int, void *),
+ void *getsfunc_data,
+ int module_index)
+{
+ return scan_script_header_err(r, buffer, MAX_STRING_LEN, getsfunc,
+ getsfunc_data, module_index);
+}
+
AP_DECLARE(int) ap_scan_script_header_err_core(request_rec *r, char *buffer,
int (*getsfunc) (char *, int, void *),
void *getsfunc_data)
@@ -830,8 +856,9 @@ AP_DECLARE(int) ap_scan_script_header_err_brigade(request_rec *r,
apr_bucket_brigade *bb,
char *buffer)
{
- return ap_scan_script_header_err_core_ex(r, buffer, getsfunc_BRIGADE, bb,
- APLOG_MODULE_INDEX);
+ return ap_scan_script_header_err_brigade_limit(r, bb, buffer,
+ APLOG_MODULE_INDEX,
+ MAX_STRING_LEN);
}
AP_DECLARE(int) ap_scan_script_header_err_brigade_ex(request_rec *r,
@@ -839,8 +866,19 @@ AP_DECLARE(int) ap_scan_script_header_err_brigade_ex(request_rec *r,
char *buffer,
int module_index)
{
- return ap_scan_script_header_err_core_ex(r, buffer, getsfunc_BRIGADE, bb,
- module_index);
+ return ap_scan_script_header_err_brigade_limit(r, bb, buffer,
+ module_index,
+ MAX_STRING_LEN);
+}
+
+AP_DECLARE(int) ap_scan_script_header_err_brigade_limit(request_rec *r,
+ apr_bucket_brigade *bb,
+ char *buffer,
+ int module_index,
+ apr_size_t line_size)
+{
+ return scan_script_header_err(r, buffer, line_size, getsfunc_BRIGADE, bb,
+ module_index);
}
diff --git a/test/modules/proxy/env.py b/test/modules/proxy/env.py
index 00610180639..3333b9b19fa 100644
--- a/test/modules/proxy/env.py
+++ b/test/modules/proxy/env.py
@@ -71,8 +71,8 @@ def __init__(self, env: 'HttpdTestEnv'):
super().__init__(env=env)
self.add_source_dir(os.path.dirname(inspect.getfile(ProxyTestSetup)))
self.add_modules(["proxy", "proxy_http", "proxy_ajp", "proxy_balancer",
- "proxy_connect", "proxy_uwsgi", "lbmethod_byrequests",
- "remoteip"])
+ "proxy_connect", "proxy_fcgi", "proxy_uwsgi",
+ "lbmethod_byrequests", "remoteip"])
class ProxyTestEnv(HttpdTestEnv):
diff --git a/test/modules/proxy/test_07_fcgi.py b/test/modules/proxy/test_07_fcgi.py
new file mode 100644
index 00000000000..0e2fbed2409
--- /dev/null
+++ b/test/modules/proxy/test_07_fcgi.py
@@ -0,0 +1,192 @@
+import socket
+import struct
+
+import pytest
+
+from pyhttpd.conf import HttpdConf
+from .env import TCPFaker
+
+FCGI_VERSION_1 = 1
+FCGI_END_REQUEST = 3
+FCGI_PARAMS = 4
+FCGI_STDIN = 5
+FCGI_STDOUT = 6
+FCGI_MAX_RECORD = 65535
+
+
+class _FCGIFaker(TCPFaker):
+ # fastcgi backend. The query string of the request selects the headers
+ # of the response:
+ # n= a header X-Big with a value of characters
+ # count= small headers X-Small-
+
+ @staticmethod
+ def _recv_exact(c, size):
+ data = b""
+ while len(data) < size:
+ chunk = c.recv(size - len(data))
+ if not chunk:
+ return None
+ data += chunk
+ return data
+
+ @staticmethod
+ def _parse_params(data):
+ params = {}
+ pos = 0
+ while pos < len(data):
+ lens = []
+ for _ in range(2):
+ if data[pos] >> 7:
+ lens.append(struct.unpack_from(">I", data, pos)[0] & 0x7fffffff)
+ pos += 4
+ else:
+ lens.append(data[pos])
+ pos += 1
+ name = data[pos:pos + lens[0]].decode("latin-1")
+ pos += lens[0]
+ params[name] = data[pos:pos + lens[1]].decode("latin-1")
+ pos += lens[1]
+ return params
+
+ @staticmethod
+ def _record(rtype, request_id, content):
+ return struct.pack(">BBHHBx", FCGI_VERSION_1, rtype, request_id,
+ len(content), 0) + content
+
+ def _read_request(self, c):
+ # records up to the empty stdin, the end of the request
+ request_id = 1
+ params = b""
+ while True:
+ head = self._recv_exact(c, 8)
+ if head is None:
+ return None, None
+ _, rtype, rid, clen, plen = struct.unpack(">BBHHBx", head)
+ body = self._recv_exact(c, clen + plen) if clen + plen else b""
+ if body is None:
+ return None, None
+ request_id = rid or request_id
+ if rtype == FCGI_PARAMS:
+ params += body[:clen]
+ elif rtype == FCGI_STDIN and clen == 0:
+ return request_id, self._parse_params(params)
+
+ def _make_output(self, query):
+ args = dict(p.split("=", 1) for p in query.split("&") if "=" in p)
+ headers = [b"Status: 200 OK", b"Content-Type: text/plain"]
+ if "n" in args:
+ headers.append(b"X-Big: " + b"a" * int(args["n"]))
+ for i in range(int(args.get("count", "0"))):
+ headers.append(f"X-Small-{i}: value".encode())
+ return b"\r\n".join(headers) + b"\r\n\r\nok"
+
+ def _process(self):
+ while not self._done:
+ try:
+ c, _ = self._socket.accept()
+ except socket.timeout:
+ continue
+ except OSError:
+ break
+ try:
+ request_id, params = self._read_request(c)
+ if request_id is None:
+ continue
+ out = self._make_output(params.get("QUERY_STRING", ""))
+ for pos in range(0, len(out), FCGI_MAX_RECORD):
+ c.sendall(self._record(FCGI_STDOUT, request_id,
+ out[pos:pos + FCGI_MAX_RECORD]))
+ c.sendall(self._record(FCGI_STDOUT, request_id, b""))
+ c.sendall(self._record(FCGI_END_REQUEST, request_id,
+ struct.pack(">IBxxx", 0, 0)))
+ except OSError:
+ pass
+ finally:
+ c.close()
+
+
+def max_value(size):
+ # A header line is read into a buffer of responsefieldsize bytes, which
+ # is 8192 by default, and can be 3 bytes shorter than that.
+ return size - 3 - len("X-Big: ")
+
+
+class TestProxyFcgi:
+
+ @pytest.fixture(autouse=True, scope='class')
+ def _class_scope(self, env):
+ env.require(env.has_shared_module("proxy_fcgi"), "mod_proxy_fcgi not available",
+ dep="mod_proxy_fcgi",
+ detected=f"no mod_proxy_fcgi.so in {env.libexec_dir}")
+ faker = _FCGIFaker("127.0.0.1", env.http_port2)
+ faker.start()
+ backend = f"fcgi://127.0.0.1:{env.http_port2}"
+ conf = HttpdConf(env)
+ conf.start_vhost(domains=[f"test1.{env.http_tld}"], port=env.http_port)
+ conf.add([
+ f"ProxyPass /default/ {backend}/default/",
+ f"ProxyPass /large/ {backend}/large/ responsefieldsize=32768",
+ f"ProxyPass /small/ {backend}/small/ responsefieldsize=4096",
+ ])
+ conf.end_vhost()
+ conf.install()
+ assert env.apache_restart() == 0
+ yield faker
+ faker.stop()
+
+ def get(self, env, path, query):
+ r = env.curl_get(env.mkurl("http", "test1", f"/{path}/?{query}"))
+ if r.response["status"] == 500:
+ # a header line which does not fit in the buffer
+ env.httpd_error_log.ignore_recent(
+ lognos=["AH01070", "AH01075"],
+ matches=[".*Premature end of script headers"]
+ )
+ return r
+
+ # small headers
+ def test_proxy_007_01(self, env):
+ r = self.get(env, "default", "n=100")
+ assert r.response["status"] == 200
+ assert r.response["header"]["x-big"] == "a" * 100
+ assert r.response["body"] == b"ok"
+
+ # a response header section larger than the buffer, of small lines
+ def test_proxy_007_02(self, env):
+ r = self.get(env, "default", "count=1000")
+ assert r.response["status"] == 200
+ assert r.response["header"]["x-small-999"] == "value"
+ assert r.response["body"] == b"ok"
+
+ # without responsefieldsize, a header line is limited to 8192 bytes
+ @pytest.mark.parametrize("n, status", [
+ [max_value(8192), 200],
+ [max_value(8192) + 1, 500],
+ ])
+ def test_proxy_007_03(self, env, n, status):
+ r = self.get(env, "default", f"n={n}")
+ assert r.response["status"] == status
+
+ # a larger responsefieldsize allows longer header lines, up to its size
+ @pytest.mark.parametrize("n, status", [
+ [20000, 200],
+ [max_value(32768), 200],
+ [max_value(32768) + 1, 500],
+ ])
+ def test_proxy_007_04(self, env, n, status):
+ r = self.get(env, "large", f"n={n}")
+ assert r.response["status"] == status
+ if status == 200:
+ assert r.response["header"]["x-big"] == "a" * n
+ assert r.response["body"] == b"ok"
+
+ # and a smaller one limits them
+ @pytest.mark.parametrize("n, status", [
+ [100, 200],
+ [max_value(4096), 200],
+ [max_value(4096) + 1, 500],
+ ])
+ def test_proxy_007_05(self, env, n, status):
+ r = self.get(env, "small", f"n={n}")
+ assert r.response["status"] == status