]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
*) mod_http2: new directive 'H2MaxDataFrameLen n' to limit the maximum
authorStefan Eissing <icing@apache.org>
Thu, 16 Feb 2023 11:58:45 +0000 (11:58 +0000)
committerStefan Eissing <icing@apache.org>
Thu, 16 Feb 2023 11:58:45 +0000 (11:58 +0000)
     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

changes-entries/h2_max_data_frame_len.txt [new file with mode: 0644]
docs/manual/mod/mod_http2.xml
modules/http2/h2_config.c
modules/http2/h2_config.h
modules/http2/h2_session.c
modules/http2/h2_session.h
modules/http2/h2_stream.c
modules/http2/h2_version.h
test/modules/http2/test_107_frame_lengths.py [new file with mode: 0644]
test/pyhttpd/nghttp.py

diff --git a/changes-entries/h2_max_data_frame_len.txt b/changes-entries/h2_max_data_frame_len.txt
new file mode 100644 (file)
index 0000000..f32f6e0
--- /dev/null
@@ -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.
index 876d6fe7c4733d2122b31875fd5d73ce98b4c1ae..68cc0908d4b7eb36f25e43e23a937577a0d6096c 100644 (file)
@@ -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>
index eea4be2c595eaea129dedbe47e1876dab25cd0ea..f6dd1065dbbcb73df747bb530537e709f06b50f2 100644 (file)
@@ -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
 };
 
index 6d2e65f926a5683b6e61c3011f9e2f056192bb18..018be64883049508e08dd06ca3e8caffee20bc63 100644 (file)
@@ -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;
index 7ba49cf8d5ee0eedfb91ead76c6b2bd9c63942ed..1d99ae61f2c8bbbf42bcff7474bd4434815f356a 100644 (file)
@@ -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);
index fbddfdd2a36ad32d9b57b4cc1b147b3ec2231854..3328509de8ac0ac390c38b707513d6252100734f 100644 (file)
@@ -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 */
     
index cf6f79897dda303cd6be170d62fd5f8e8b8d4779..c514df6499405d6fd639381624d6b30047fe50f9 100644 (file)
@@ -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. */
index 0caa8003873eec4af804bf40d3e0c9dae2f55463..380818bbc41f4929d69b249a233c3940535d63f8 100644 (file)
@@ -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 (file)
index 0000000..d636093
--- /dev/null
@@ -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"]}'
index fe4a1aedff366df7d24e6355464cc740ebe2b489..3c9b0c4444e5dda25e4c8c0166ef82ed76cb1b9e 100644 (file)
@@ -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):