summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--changes-entries/h2_max_data_frame_len.txt7
-rw-r--r--docs/manual/mod/mod_http2.xml25
-rw-r--r--modules/http2/h2_config.c22
-rw-r--r--modules/http2/h2_config.h1
-rw-r--r--modules/http2/h2_session.c9
-rw-r--r--modules/http2/h2_session.h3
-rw-r--r--modules/http2/h2_stream.c5
-rw-r--r--modules/http2/h2_version.h4
-rw-r--r--test/modules/http2/test_107_frame_lengths.py51
-rw-r--r--test/pyhttpd/nghttp.py5
10 files changed, 125 insertions, 7 deletions
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 0000000000..f32f6e076e
--- /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 876d6fe7c4..68cc0908d4 100644
--- a/docs/manual/mod/mod_http2.xml
+++ b/docs/manual/mod/mod_http2.xml
@@ -1024,4 +1024,29 @@ H2TLSCoolDownSecs 0
</usage>
</directivesynopsis>
+ <directivesynopsis>
+ <name>H2MaxDataFrameLen</name>
+ <description>Maximum bytes inside a single HTTP/2 DATA frame</description>
+ <syntax>H2MaxDataFrameLen <em>n</em></syntax>
+ <default>H2MaxDataFrameLen 0</default>
+ <contextlist>
+ <context>server config</context>
+ <context>virtual host</context>
+ </contextlist>
+ <compatibility>Available in version 2.5.1 and later.</compatibility>
+
+ <usage>
+ <p>
+ <directive>H2MaxDataFrameLen</directive> 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).
+ </p><p>
+ 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.
+ </p>
+ </usage>
+ </directivesynopsis>
+
</modulesynopsis>
diff --git a/modules/http2/h2_config.c b/modules/http2/h2_config.c
index eea4be2c59..f6dd1065db 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 6d2e65f926..018be64883 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 7ba49cf8d5..1d99ae61f2 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 fbddfdd2a3..3328509de8 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 cf6f79897d..c514df6499 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 0caa800387..380818bbc4 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 0000000000..d6360939be
--- /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 fe4a1aedff..3c9b0c4444 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):