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