Skip to content

Commit

Permalink
*) mod_http2: new directive 'H2MaxDataFrameLen n' to limit the maximum
Browse files Browse the repository at this point in the history
     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
  • Loading branch information
icing committed Feb 16, 2023
1 parent 46fff96 commit ff6b802
Show file tree
Hide file tree
Showing 10 changed files with 125 additions and 7 deletions.
7 changes: 7 additions & 0 deletions changes-entries/h2_max_data_frame_len.txt
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions docs/manual/mod/mod_http2.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
22 changes: 22 additions & 0 deletions modules/http2/h2_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
};

Expand Down
1 change: 1 addition & 0 deletions modules/http2/h2_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
9 changes: 6 additions & 3 deletions modules/http2/h2_session.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
3 changes: 2 additions & 1 deletion modules/http2/h2_session.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 */

Expand Down
5 changes: 5 additions & 0 deletions modules/http2/h2_stream.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
4 changes: 2 additions & 2 deletions modules/http2/h2_version.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
* @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
* Numerical representation of the version number of the http2 module
* 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 */
51 changes: 51 additions & 0 deletions test/modules/http2/test_107_frame_lengths.py
Original file line number Diff line number Diff line change
@@ -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"]}'
5 changes: 4 additions & 1 deletion test/pyhttpd/nghttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def get_stream(streams, sid):
"id": sid,
"body": b''
},
"data_lengths": [],
"paddings": [],
"promises": []
}
Expand Down Expand Up @@ -131,12 +132,13 @@ def parse_output(self, btext) -> Dict:
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()
Expand Down Expand Up @@ -196,6 +198,7 @@ def parse_output(self, btext) -> Dict:
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):
Expand Down

0 comments on commit ff6b802

Please sign in to comment.