From: Stefan Eissing Date: Thu, 16 Feb 2023 11:58:45 +0000 (+0000) Subject: *) mod_http2: new directive 'H2MaxDataFrameLen n' to limit the maximum X-Git-Tag: 2.5.0-alpha2-ci-test-only~101 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ff6b8026acb8610e4faf10ee345141a3da85946e;p=thirdparty%2Fapache%2Fhttpd.git *) mod_http2: new directive 'H2MaxDataFrameLen n' to limit the maximum amount of response body bytes put into a single HTTP/2 DATA frame. Setting this to 0 places no limit (but the max size allowed by the protocol is observed). The module, by default, tries to use the maximum size possible, which is somewhat around 16KB. This sets the maximum. When less response data is available, smaller frames will be sent. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1907697 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/changes-entries/h2_max_data_frame_len.txt b/changes-entries/h2_max_data_frame_len.txt new file mode 100644 index 00000000000..f32f6e076e4 --- /dev/null +++ b/changes-entries/h2_max_data_frame_len.txt @@ -0,0 +1,7 @@ + *) mod_http2: new directive 'H2MaxDataFrameLen n' to limit the maximum + amount of response body bytes put into a single HTTP/2 DATA frame. + Setting this to 0 places no limit (but the max size allowed by the + protocol is observed). + The module, by default, tries to use the maximum size possible, which is + somewhat around 16KB. This sets the maximum. When less response data is + available, smaller frames will be sent. diff --git a/docs/manual/mod/mod_http2.xml b/docs/manual/mod/mod_http2.xml index 876d6fe7c47..68cc0908d4b 100644 --- a/docs/manual/mod/mod_http2.xml +++ b/docs/manual/mod/mod_http2.xml @@ -1024,4 +1024,29 @@ H2TLSCoolDownSecs 0 + + H2MaxDataFrameLen + Maximum bytes inside a single HTTP/2 DATA frame + H2MaxDataFrameLen n + H2MaxDataFrameLen 0 + + server config + virtual host + + Available in version 2.5.1 and later. + + +

+ H2MaxDataFrameLen limits the maximum + amount of response body bytes placed into a single HTTP/2 DATA + frame. Setting this to 0 places no limit (but the max size + allowed by the protocol is observed). +

+ The module, by default, tries to use the maximum size possible, + which is somewhat around 16KB. This sets the maximum. When less + response data is availble, smaller frames will be sent. +

+
+
+ diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c index eea4be2c595..f6dd1065dbb 100644 --- a/modules/http2/h2_config.c +++ b/modules/http2/h2_config.c @@ -75,6 +75,7 @@ typedef struct h2_config { int padding_always; int output_buffered; apr_interval_time_t stream_timeout;/* beam timeout */ + int max_data_frame_len; /* max # bytes in a single h2 DATA frame */ } h2_config; typedef struct h2_dir_config { @@ -110,6 +111,7 @@ static h2_config defconf = { 1, /* padding always */ 1, /* stream output buffered */ -1, /* beam timeout */ + 0, /* max DATA frame len, 0 == no extra limit */ }; static h2_dir_config defdconf = { @@ -153,6 +155,7 @@ void *h2_config_create_svr(apr_pool_t *pool, server_rec *s) conf->padding_always = DEF_VAL; conf->output_buffered = DEF_VAL; conf->stream_timeout = DEF_VAL; + conf->max_data_frame_len = DEF_VAL; return conf; } @@ -195,6 +198,7 @@ static void *h2_config_merge(apr_pool_t *pool, void *basev, void *addv) n->padding_bits = H2_CONFIG_GET(add, base, padding_bits); n->padding_always = H2_CONFIG_GET(add, base, padding_always); n->stream_timeout = H2_CONFIG_GET(add, base, stream_timeout); + n->max_data_frame_len = H2_CONFIG_GET(add, base, max_data_frame_len); return n; } @@ -278,6 +282,8 @@ static apr_int64_t h2_srv_config_geti64(const h2_config *conf, h2_config_var_t v return H2_CONFIG_GET(conf, &defconf, output_buffered); case H2_CONF_STREAM_TIMEOUT: return H2_CONFIG_GET(conf, &defconf, stream_timeout); + case H2_CONF_MAX_DATA_FRAME_LEN: + return H2_CONFIG_GET(conf, &defconf, max_data_frame_len); default: return DEF_VAL; } @@ -337,6 +343,9 @@ static void h2_srv_config_seti(h2_config *conf, h2_config_var_t var, int val) case H2_CONF_OUTPUT_BUFFER: H2_CONFIG_SET(conf, output_buffered, val); break; + case H2_CONF_MAX_DATA_FRAME_LEN: + H2_CONFIG_SET(conf, max_data_frame_len, val); + break; default: break; } @@ -583,6 +592,17 @@ static const char *h2_conf_set_stream_max_mem_size(cmd_parms *cmd, return NULL; } +static const char *h2_conf_set_max_data_frame_len(cmd_parms *cmd, + void *dirconf, const char *value) +{ + int val = (int)apr_atoi64(value); + if (val < 0) { + return "value must be 0 or larger"; + } + CONFIG_CMD_SET(cmd, dirconf, H2_CONF_MAX_DATA_FRAME_LEN, val); + return NULL; +} + static const char *h2_conf_set_session_extra_files(cmd_parms *cmd, void *dirconf, const char *value) { @@ -937,6 +957,8 @@ const command_rec h2_cmds[] = { RSRC_CONF, "set stream output buffer on/off"), AP_INIT_TAKE1("H2StreamTimeout", h2_conf_set_stream_timeout, NULL, RSRC_CONF, "set stream timeout"), + AP_INIT_TAKE1("H2MaxDataFrameLen", h2_conf_set_max_data_frame_len, NULL, + RSRC_CONF, "maximum number of bytes in a single HTTP/2 DATA frame"), AP_END_CMD }; diff --git a/modules/http2/h2_config.h b/modules/http2/h2_config.h index 6d2e65f926a..018be648830 100644 --- a/modules/http2/h2_config.h +++ b/modules/http2/h2_config.h @@ -43,6 +43,7 @@ typedef enum { H2_CONF_PADDING_ALWAYS, H2_CONF_OUTPUT_BUFFER, H2_CONF_STREAM_TIMEOUT, + H2_CONF_MAX_DATA_FRAME_LEN, } h2_config_var_t; struct apr_hash_t; diff --git a/modules/http2/h2_session.c b/modules/http2/h2_session.c index 7ba49cf8d5e..1d99ae61f2c 100644 --- a/modules/http2/h2_session.c +++ b/modules/http2/h2_session.c @@ -902,7 +902,8 @@ apr_status_t h2_session_create(h2_session **psession, conn_rec *c, request_rec * session->max_stream_count = h2_config_sgeti(s, H2_CONF_MAX_STREAMS); session->max_stream_mem = h2_config_sgeti(s, H2_CONF_STREAM_MAX_MEM); - + session->max_data_frame_len = h2_config_sgeti(s, H2_CONF_MAX_DATA_FRAME_LEN); + session->out_c1_blocked = h2_iq_create(session->pool, (int)session->max_stream_count); session->ready_to_process = h2_iq_create(session->pool, (int)session->max_stream_count); @@ -983,13 +984,15 @@ apr_status_t h2_session_create(h2_session **psession, conn_rec *c, request_rec * H2_SSSN_LOG(APLOGNO(03200), session, "created, max_streams=%d, stream_mem=%d, " "workers_limit=%d, workers_max=%d, " - "push_diary(type=%d,N=%d)"), + "push_diary(type=%d,N=%d), " + "max_data_frame_len=%d"), (int)session->max_stream_count, (int)session->max_stream_mem, session->mplx->processing_limit, session->mplx->processing_max, session->push_diary->dtype, - (int)session->push_diary->N); + (int)session->push_diary->N, + (int)session->max_data_frame_len); } apr_pool_pre_cleanup_register(pool, c, session_pool_cleanup); diff --git a/modules/http2/h2_session.h b/modules/http2/h2_session.h index fbddfdd2a36..3328509de8a 100644 --- a/modules/http2/h2_session.h +++ b/modules/http2/h2_session.h @@ -103,7 +103,8 @@ typedef struct h2_session { apr_size_t max_stream_count; /* max number of open streams */ apr_size_t max_stream_mem; /* max buffer memory for a single stream */ - + apr_size_t max_data_frame_len; /* max amount of bytes for a single DATA frame */ + apr_size_t idle_frames; /* number of rcvd frames that kept session in idle state */ apr_interval_time_t idle_delay; /* Time we delay processing rcvd frames in idle state */ diff --git a/modules/http2/h2_stream.c b/modules/http2/h2_stream.c index cf6f79897dd..c514df64994 100644 --- a/modules/http2/h2_stream.c +++ b/modules/http2/h2_stream.c @@ -1361,6 +1361,11 @@ static ssize_t stream_data_cb(nghttp2_session *ng2s, length = chunk_len; } } + /* We allow configurable max DATA frame length. */ + if (stream->session->max_data_frame_len > 0 + && length > stream->session->max_data_frame_len) { + length = stream->session->max_data_frame_len; + } /* How much data do we have in our buffers that we can write? * if not enough, receive more. */ diff --git a/modules/http2/h2_version.h b/modules/http2/h2_version.h index 0caa8003873..380818bbc41 100644 --- a/modules/http2/h2_version.h +++ b/modules/http2/h2_version.h @@ -27,7 +27,7 @@ * @macro * Version number of the http2 module as c string */ -#define MOD_HTTP2_VERSION "2.0.12" +#define MOD_HTTP2_VERSION "2.0.13" /** * @macro @@ -35,7 +35,7 @@ * release. This is a 24 bit number with 8 bits for major number, 8 bits * for minor and 8 bits for patch. Version 1.2.3 becomes 0x010203. */ -#define MOD_HTTP2_VERSION_NUM 0x02000c +#define MOD_HTTP2_VERSION_NUM 0x02000d #endif /* mod_h2_h2_version_h */ diff --git a/test/modules/http2/test_107_frame_lengths.py b/test/modules/http2/test_107_frame_lengths.py new file mode 100644 index 00000000000..d6360939bea --- /dev/null +++ b/test/modules/http2/test_107_frame_lengths.py @@ -0,0 +1,51 @@ +import os +import pytest + +from .env import H2Conf, H2TestEnv + + +def mk_text_file(fpath: str, lines: int): + t110 = "" + for _ in range(11): + t110 += "0123456789" + with open(fpath, "w") as fd: + for i in range(lines): + fd.write("{0:015d}: ".format(i)) # total 128 bytes per line + fd.write(t110) + fd.write("\n") + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestFrameLengths: + + URI_PATHS = [] + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + docs_a = os.path.join(env.server_docs_dir, "cgi/files") + for fsize in [10, 100]: + fname = f'0-{fsize}k.txt' + mk_text_file(os.path.join(docs_a, fname), 8 * fsize) + self.URI_PATHS.append(f"/files/{fname}") + + @pytest.mark.parametrize("data_frame_len", [ + 99, 1024, 8192 + ]) + def test_h2_107_01(self, env, data_frame_len): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + f'H2MaxDataFrameLen {data_frame_len}', + ] + }) + conf.add_vhost_cgi() + conf.install() + assert env.apache_restart() == 0 + for p in self.URI_PATHS: + url = env.mkurl("https", "cgi", p) + r = env.nghttp().get(url, options=[ + '--header=Accept-Encoding: none', + ]) + assert r.response["status"] == 200 + assert len(r.results["data_lengths"]) > 0, f'{r}' + too_large = [ x for x in r.results["data_lengths"] if x > data_frame_len] + assert len(too_large) == 0, f'{p}: {r.results["data_lengths"]}' diff --git a/test/pyhttpd/nghttp.py b/test/pyhttpd/nghttp.py index fe4a1aedff3..3c9b0c4444e 100644 --- a/test/pyhttpd/nghttp.py +++ b/test/pyhttpd/nghttp.py @@ -37,6 +37,7 @@ class Nghttp: "id": sid, "body": b'' }, + "data_lengths": [], "paddings": [], "promises": [] } @@ -131,12 +132,13 @@ class Nghttp: s = self.get_stream(streams, m.group(3)) blen = int(m.group(2)) if s: - print("stream %d: %d DATA bytes added" % (s["id"], blen)) + print(f'stream {s["id"]}: {blen} DATA bytes added via "{l}"') padlen = 0 if len(lines) > lidx + 2: mpad = re.match(r' +\(padlen=(\d+)\)', lines[lidx+2]) if mpad: padlen = int(mpad.group(1)) + s["data_lengths"].append(blen) s["paddings"].append(padlen) blen -= padlen s["response"]["body"] += body[-blen:].encode() @@ -196,6 +198,7 @@ class Nghttp: if main_stream in streams: output["response"] = streams[main_stream]["response"] output["paddings"] = streams[main_stream]["paddings"] + output["data_lengths"] = streams[main_stream]["data_lengths"] return output def _raw(self, url, timeout, options):