You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2021/08/20 16:07:45 UTC

svn commit: r1892476 [11/12] - in /httpd/httpd/trunk: ./ test/ test/modules/ test/modules/http2/ test/modules/http2/conf/ test/modules/http2/data/ test/modules/http2/htdocs/ test/modules/http2/htdocs/cgi/ test/modules/http2/htdocs/cgi/files/ test/modul...

Added: httpd/httpd/trunk/test/modules/http2/test_004_post.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_004_post.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_004_post.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_004_post.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,217 @@
+import email.parser
+import json
+import os
+import re
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        env.setup_data_1k_1m()
+        HttpdConf(env).add_vhost_cgi().install()
+        assert env.apache_restart() == 0
+
+    # upload and GET again using curl, compare to original content
+    def curl_upload_and_verify(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/upload.py")
+        fpath = os.path.join(env.gen_dir, fname)
+        r = env.curl_upload(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert r.response["status"] >= 200 and r.response["status"] < 300
+
+        r2 = env.curl_get(r.response["header"]["location"])
+        assert r2.exit_code == 0
+        assert r2.response["status"] == 200
+        with open(env.test_src(fpath), mode='rb') as file:
+            src = file.read()
+        assert src == r2.response["body"]
+
+    def test_004_01(self, env):
+        self.curl_upload_and_verify(env, "data-1k", ["--http1.1"])
+        self.curl_upload_and_verify(env, "data-1k", ["--http2"])
+
+    def test_004_02(self, env):
+        self.curl_upload_and_verify(env, "data-10k", ["--http1.1"])
+        self.curl_upload_and_verify(env, "data-10k", ["--http2"])
+
+    def test_004_03(self, env):
+        self.curl_upload_and_verify(env, "data-100k", ["--http1.1"])
+        self.curl_upload_and_verify(env, "data-100k", ["--http2"])
+
+    def test_004_04(self, env):
+        self.curl_upload_and_verify(env, "data-1m", ["--http1.1"])
+        self.curl_upload_and_verify(env, "data-1m", ["--http2"])
+
+    def test_004_05(self, env):
+        self.curl_upload_and_verify(env, "data-1k", ["-v", "--http1.1", "-H", "Expect: 100-continue"])
+        self.curl_upload_and_verify(env, "data-1k", ["-v", "--http2", "-H", "Expect: 100-continue"])
+
+    @pytest.mark.skipif(True, reason="python3 regresses in chunked inputs to cgi")
+    def test_004_06(self, env):
+        self.curl_upload_and_verify(env, "data-1k", ["--http1.1", "-H", "Content-Length: "])
+        self.curl_upload_and_verify(env, "data-1k", ["--http2", "-H", "Content-Length: "])
+
+    @pytest.mark.parametrize("name, value", [
+        ("HTTP2", "on"),
+        ("H2PUSH", "off"),
+        ("H2_PUSHED", ""),
+        ("H2_PUSHED_ON", ""),
+        ("H2_STREAM_ID", "1"),
+        ("H2_STREAM_TAG", r'\d+-1'),
+    ])
+    def test_004_07(self, env, name, value):
+        url = env.mkurl("https", "cgi", "/env.py")
+        r = env.curl_post_value(url, "name", name)
+        assert r.exit_code == 0
+        assert r.response["status"] == 200
+        m = re.match("{0}=(.*)".format(name), r.response["body"].decode('utf-8'))
+        assert m
+        assert re.match(value, m.group(1)) 
+
+    # verify that we parse nghttp output correctly
+    def check_nghttp_body(self, env, ref_input, nghttp_output):
+        with open(env.test_src(os.path.join(env.gen_dir, ref_input)), mode='rb') as f:
+            refbody = f.read()
+        with open(env.test_src(nghttp_output), mode='rb') as f:
+            text = f.read()
+        o = env.nghttp().parse_output(text)
+        assert "response" in o
+        assert "body" in o["response"]
+        if refbody != o["response"]["body"]:
+            with open(env.test_src(os.path.join(env.gen_dir, '%s.parsed' % ref_input)), mode='bw') as f:
+                f.write(o["response"]["body"])
+        assert len(refbody) == len(o["response"]["body"])
+        assert refbody == o["response"]["body"]
+    
+    def test_004_20(self, env):
+        self.check_nghttp_body(env, 'data-1k', 'data/nghttp-output-1k-1.txt')
+        self.check_nghttp_body(env, 'data-10k', 'data/nghttp-output-10k-1.txt')
+        self.check_nghttp_body(env, 'data-100k', 'data/nghttp-output-100k-1.txt')
+
+    # POST some data using nghttp and see it echo'ed properly back
+    def nghttp_post_and_verify(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/echo.py")
+        fpath = os.path.join(env.gen_dir, fname)
+
+        r = env.nghttp().upload(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert r.response["status"] >= 200 and r.response["status"] < 300
+
+        with open(env.test_src(fpath), mode='rb') as file:
+            src = file.read()
+        assert src == r.response["body"]
+
+    def test_004_21(self, env):
+        self.nghttp_post_and_verify(env, "data-1k", [])
+        self.nghttp_post_and_verify(env, "data-10k", [])
+        self.nghttp_post_and_verify(env, "data-100k", [])
+        self.nghttp_post_and_verify(env, "data-1m", [])
+
+    def test_004_22(self, env):
+        self.nghttp_post_and_verify(env, "data-1k", ["--no-content-length"])
+        self.nghttp_post_and_verify(env, "data-10k", ["--no-content-length"])
+        self.nghttp_post_and_verify(env, "data-100k", ["--no-content-length"])
+        self.nghttp_post_and_verify(env, "data-1m", ["--no-content-length"])
+
+    # upload and GET again using nghttp, compare to original content
+    def nghttp_upload_and_verify(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/upload.py")
+        fpath = os.path.join(env.gen_dir, fname)
+
+        r = env.nghttp().upload_file(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert r.response["status"] >= 200 and r.response["status"] < 300
+        assert r.response["header"]["location"]
+
+        r2 = env.nghttp().get(r.response["header"]["location"])
+        assert r2.exit_code == 0
+        assert r2.response["status"] == 200
+        with open(env.test_src(fpath), mode='rb') as file:
+            src = file.read()
+        assert src == r2.response["body"]
+
+    def test_004_23(self, env):
+        self.nghttp_upload_and_verify(env, "data-1k", [])
+        self.nghttp_upload_and_verify(env, "data-10k", [])
+        self.nghttp_upload_and_verify(env, "data-100k", [])
+        self.nghttp_upload_and_verify(env, "data-1m", [])
+
+    def test_004_24(self, env):
+        self.nghttp_upload_and_verify(env, "data-1k", ["--expect-continue"])
+        self.nghttp_upload_and_verify(env, "data-100k", ["--expect-continue"])
+
+    @pytest.mark.skipif(True, reason="python3 regresses in chunked inputs to cgi")
+    def test_004_25(self, env):
+        self.nghttp_upload_and_verify(env, "data-1k", ["--no-content-length"])
+        self.nghttp_upload_and_verify(env, "data-10k", ["--no-content-length"])
+        self.nghttp_upload_and_verify(env, "data-100k", ["--no-content-length"])
+        self.nghttp_upload_and_verify(env, "data-1m", ["--no-content-length"])
+
+    def test_004_30(self, env):
+        # issue: #203
+        resource = "data-1k"
+        full_length = 1000
+        chunk = 200
+        self.curl_upload_and_verify(env, resource, ["-v", "--http2"])
+        logfile = os.path.join(env.server_logs_dir, "test_004_30")
+        if os.path.isfile(logfile):
+            os.remove(logfile)
+        HttpdConf(env).add("""
+LogFormat "{ \\"request\\": \\"%r\\", \\"status\\": %>s, \\"bytes_resp_B\\": %B, \\"bytes_tx_O\\": %O, \\"bytes_rx_I\\": %I, \\"bytes_rx_tx_S\\": %S }" issue_203
+CustomLog logs/test_004_30 issue_203
+        """).add_vhost_cgi().install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/files/{0}".format(resource))
+        r = env.curl_get(url, 5, ["--http2"])
+        assert 200 == r.response["status"]
+        r = env.curl_get(url, 5, ["--http1.1", "-H", "Range: bytes=0-{0}".format(chunk-1)])
+        assert 206 == r.response["status"]
+        assert chunk == len(r.response["body"].decode('utf-8'))
+        r = env.curl_get(url, 5, ["--http2", "-H", "Range: bytes=0-{0}".format(chunk-1)])
+        assert 206 == r.response["status"]
+        assert chunk == len(r.response["body"].decode('utf-8'))
+        # now check what response lengths have actually been reported
+        lines = open(logfile).readlines()
+        log_h2_full = json.loads(lines[-3])
+        log_h1 = json.loads(lines[-2])
+        log_h2 = json.loads(lines[-1])
+        assert log_h2_full['bytes_rx_I'] > 0
+        assert log_h2_full['bytes_resp_B'] == full_length
+        assert log_h2_full['bytes_tx_O'] > full_length
+        assert log_h1['bytes_rx_I'] > 0         # input bytes recieved
+        assert log_h1['bytes_resp_B'] == chunk  # response bytes sent (payload)
+        assert log_h1['bytes_tx_O'] > chunk     # output bytes sent
+        assert log_h2['bytes_rx_I'] > 0
+        assert log_h2['bytes_resp_B'] == chunk
+        assert log_h2['bytes_tx_O'] > chunk
+        
+    def test_004_40(self, env):
+        # echo content using h2test_module "echo" handler
+        def post_and_verify(fname, options=None):
+            url = env.mkurl("https", "cgi", "/h2test/echo")
+            fpath = os.path.join(env.gen_dir, fname)
+            r = env.curl_upload(url, fpath, options=options)
+            assert r.exit_code == 0
+            assert r.response["status"] >= 200 and r.response["status"] < 300
+            
+            ct = r.response["header"]["content-type"]
+            mail_hd = "Content-Type: " + ct + "\r\nMIME-Version: 1.0\r\n\r\n"
+            mime_msg = mail_hd.encode() + r.response["body"]
+            # this MIME API is from hell
+            body = email.parser.BytesParser().parsebytes(mime_msg)
+            assert body
+            assert body.is_multipart()
+            filepart = None
+            for part in body.walk():
+                if fname == part.get_filename():
+                    filepart = part
+            assert filepart
+            with open(env.test_src(fpath), mode='rb') as file:
+                src = file.read()
+            assert src == filepart.get_payload(decode=True)
+        
+        post_and_verify("data-1k", [])

Added: httpd/httpd/trunk/test/modules/http2/test_005_status.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_005_status.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_005_status.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_005_status.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,70 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_cgi().install()
+        assert env.apache_restart() == 0
+
+    def test_005_01(self, env):
+        url = env.mkurl("https", "cgi", "/.well-known/h2/state")
+        r = env.curl_get(url, 5)
+        assert 200 == r.response["status"]
+        st = r.response["json"]
+        
+        # remove some parts that are very dependant on client/lib versions
+        # or connection time etc.
+        del st["settings"]["SETTINGS_INITIAL_WINDOW_SIZE"]
+        del st["peerSettings"]["SETTINGS_INITIAL_WINDOW_SIZE"]
+        del st["streams"]["1"]["created"]
+        del st["streams"]["1"]["flowOut"]
+        del st["stats"]["in"]["frames"]
+        del st["stats"]["in"]["octets"]
+        del st["stats"]["out"]["frames"]
+        del st["stats"]["out"]["octets"]
+        del st["connFlowOut"]
+        
+        assert st == {
+            "version": "draft-01",
+            "settings": {
+                "SETTINGS_MAX_CONCURRENT_STREAMS": 100,
+                "SETTINGS_MAX_FRAME_SIZE": 16384,
+                "SETTINGS_ENABLE_PUSH": 0
+            },
+            "peerSettings": {
+                "SETTINGS_MAX_CONCURRENT_STREAMS": 100,
+                "SETTINGS_MAX_FRAME_SIZE": 16384,
+                "SETTINGS_ENABLE_PUSH": 0,
+                "SETTINGS_HEADER_TABLE_SIZE": 4096,
+                "SETTINGS_MAX_HEADER_LIST_SIZE": -1
+            },
+            "connFlowIn": 2147483647,
+            "sentGoAway": 0,
+            "streams": {
+                "1": {
+                    "state": "HALF_CLOSED_REMOTE",
+                    "flowIn": 65535,
+                    "dataIn": 0,
+                    "dataOut": 0
+                }
+            },
+            "stats": {
+                "in": {
+                    "requests": 1,
+                    "resets": 0, 
+                },
+                "out": {
+                    "responses": 0,
+                },
+                "push": {
+                    "cacheDigest": "AQg",
+                    "promises": 0,
+                    "submits": 0,
+                    "resets": 0
+                }
+            }
+        }

Added: httpd/httpd/trunk/test/modules/http2/test_006_assets.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_006_assets.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_006_assets.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_006_assets.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,74 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    # single page without any assets
+    def test_006_01(self, env):
+        url = env.mkurl("https", "test1", "/001.html")
+        r = env.nghttp().assets(url,  options=["-Haccept-encoding: none"])
+        assert 0 == r.exit_code
+        assert 1 == len(r.assets)
+        assert r.assets == [
+            {"status": 200, "size": "251", "path": "/001.html"}
+        ]
+
+    # single image without any assets
+    def test_006_02(self, env):
+        url = env.mkurl("https", "test1", "/002.jpg")
+        r = env.nghttp().assets(url,  options=["-Haccept-encoding: none"])
+        assert 0 == r.exit_code
+        assert 1 == len(r.assets)
+        assert r.assets == [
+            {"status": 200, "size": "88K", "path": "/002.jpg"}
+        ]
+        
+    # gophertiles, yea!
+    def test_006_03(self, env):
+        # create the tiles files we originally had checked in
+        exp_assets = [
+            {"status": 200, "size": "10K", "path": "/004.html"},
+            {"status": 200, "size": "742", "path": "/004/gophertiles.jpg"},
+        ]
+        for i in range(2, 181):
+            with open(f"{env.server_docs_dir}/test1/004/gophertiles_{i:03d}.jpg", "w") as fd:
+                fd.write("0123456789\n")
+            exp_assets.append(
+                {"status": 200, "size": "11", "path": f"/004/gophertiles_{i:03d}.jpg"},
+            )
+
+        url = env.mkurl("https", "test1", "/004.html")
+        r = env.nghttp().assets(url, options=["-Haccept-encoding: none"])
+        assert 0 == r.exit_code
+        assert 181 == len(r.assets)
+        assert r.assets == exp_assets
+            
+    # page with js and css
+    def test_006_04(self, env):
+        url = env.mkurl("https", "test1", "/006.html")
+        r = env.nghttp().assets(url, options=["-Haccept-encoding: none"])
+        assert 0 == r.exit_code
+        assert 3 == len(r.assets)
+        assert r.assets == [
+            {"status": 200, "size": "543", "path": "/006.html"},
+            {"status": 200, "size": "216", "path": "/006/006.css"},
+            {"status": 200, "size": "839", "path": "/006/006.js"}
+        ]
+
+    # page with image, try different window size
+    def test_006_05(self, env):
+        url = env.mkurl("https", "test1", "/003.html")
+        r = env.nghttp().assets(url, options=["--window-bits=24", "-Haccept-encoding: none"])
+        assert 0 == r.exit_code
+        assert 2 == len(r.assets)
+        assert r.assets == [
+            {"status": 200, "size": "316", "path": "/003.html"},
+            {"status": 200, "size": "88K", "path": "/003/003_img.jpg"}
+        ]

Added: httpd/httpd/trunk/test/modules/http2/test_100_conn_reuse.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_100_conn_reuse.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_100_conn_reuse.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_100_conn_reuse.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,56 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_noh2().add_vhost_test1().add_vhost_cgi().install()
+        assert env.apache_restart() == 0
+
+    # make sure the protocol selection on the different hosts work as expected
+    def test_100_01(self, env):
+        # this host defaults to h2, but we can request h1
+        url = env.mkurl("https", "cgi", "/hello.py")
+        assert "2" == env.curl_protocol_version( url )
+        assert "1.1" == env.curl_protocol_version( url, options=[ "--http1.1" ] )
+        
+        # this host does not enable h2, it always falls back to h1
+        url = env.mkurl("https", "noh2", "/hello.py")
+        assert "1.1" == env.curl_protocol_version( url )
+        assert "1.1" == env.curl_protocol_version( url, options=[ "--http2" ] )
+
+    # access a ServerAlias, after using ServerName in SNI
+    def test_100_02(self, env):
+        url = env.mkurl("https", "cgi", "/hello.py")
+        hostname = ("cgi-alias.%s" % env.http_tld)
+        r = env.curl_get(url, 5, [ "-H", "Host:%s" % hostname ])
+        assert 200 == r.response["status"]
+        assert "HTTP/2" == r.response["protocol"]
+        assert hostname == r.response["json"]["host"]
+
+    # access another vhost, after using ServerName in SNI, that uses same SSL setup
+    def test_100_03(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        hostname = ("test1.%s" % env.http_tld)
+        r = env.curl_get(url, 5, [ "-H", "Host:%s" % hostname ])
+        assert 200 == r.response["status"]
+        assert "HTTP/2" == r.response["protocol"]
+        assert "text/html" == r.response["header"]["content-type"]
+
+    # access another vhost, after using ServerName in SNI, 
+    # that has different SSL certificate. This triggers a 421 (misdirected request) response.
+    def test_100_04(self, env):
+        url = env.mkurl("https", "cgi", "/hello.py")
+        hostname = ("noh2.%s" % env.http_tld)
+        r = env.curl_get(url, 5, [ "-H", "Host:%s" % hostname ])
+        assert 421 == r.response["status"]
+
+    # access an unknown vhost, after using ServerName in SNI
+    def test_100_05(self, env):
+        url = env.mkurl("https", "cgi", "/hello.py")
+        hostname = ("unknown.%s" % env.http_tld)
+        r = env.curl_get(url, 5, [ "-H", "Host:%s" % hostname ])
+        assert 421 == r.response["status"]

Added: httpd/httpd/trunk/test/modules/http2/test_101_ssl_reneg.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_101_ssl_reneg.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_101_ssl_reneg.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_101_ssl_reneg.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,126 @@
+import re
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add(
+            f"""
+            SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384
+            <Directory \"{env.server_dir}/htdocs/ssl-client-verify\"> 
+                Require all granted
+                SSLVerifyClient require
+                SSLVerifyDepth 0
+            </Directory>"""
+        ).start_vhost(
+            env.https_port, "ssl", with_ssl=True
+        ).add(
+            f"""
+            Protocols h2 http/1.1"
+            <Location /renegotiate/cipher>
+                SSLCipherSuite ECDHE-RSA-CHACHA20-POLY1305
+            </Location>
+            <Location /renegotiate/err-doc-cipher>
+                SSLCipherSuite ECDHE-RSA-CHACHA20-POLY1305
+                ErrorDocument 403 /forbidden.html
+            </Location>
+            <Location /renegotiate/verify>
+                SSLVerifyClient require
+            </Location>
+            <Directory \"{env.server_dir}/htdocs/sslrequire\"> 
+                SSLRequireSSL
+            </Directory>
+            <Directory \"{env.server_dir}/htdocs/requiressl\"> 
+                Require ssl
+            </Directory>"""
+        ).end_vhost().install()
+        # the dir needs to exists for the configuration to have effect
+        env.mkpath("%s/htdocs/ssl-client-verify" % env.server_dir)
+        env.mkpath("%s/htdocs/renegotiate/cipher" % env.server_dir)
+        env.mkpath("%s/htdocs/sslrequire" % env.server_dir)
+        env.mkpath("%s/htdocs/requiressl" % env.server_dir)
+        assert env.apache_restart() == 0
+
+    # access a resource with SSL renegotiation, using HTTP/1.1
+    def test_101_01(self, env):
+        url = env.mkurl("https", "ssl", "/renegotiate/cipher/")
+        r = env.curl_get(url, options=["-v", "--http1.1", "--tlsv1.2", "--tls-max", "1.2"])
+        assert 0 == r.exit_code
+        assert r.response
+        assert 403 == r.response["status"]
+        
+    # try to renegotiate the cipher, should fail with correct code
+    def test_101_02(self, env):
+        url = env.mkurl("https", "ssl", "/renegotiate/cipher/")
+        r = env.curl_get(url, options=[
+            "-vvv", "--tlsv1.2", "--tls-max", "1.2", "--ciphers", "ECDHE-RSA-AES256-GCM-SHA384"
+        ])
+        assert 0 != r.exit_code
+        assert not r.response
+        assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        
+    # try to renegotiate a client certificate from Location 
+    # needs to fail with correct code
+    def test_101_03(self, env):
+        url = env.mkurl("https", "ssl", "/renegotiate/verify/")
+        r = env.curl_get(url, options=["-vvv", "--tlsv1.2", "--tls-max", "1.2"])
+        assert 0 != r.exit_code
+        assert not r.response
+        assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        
+    # try to renegotiate a client certificate from Directory 
+    # needs to fail with correct code
+    def test_101_04(self, env):
+        url = env.mkurl("https", "ssl", "/ssl-client-verify/index.html")
+        r = env.curl_get(url, options=["-vvv", "--tlsv1.2", "--tls-max", "1.2"])
+        assert 0 != r.exit_code
+        assert not r.response
+        assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        
+    # make 10 requests on the same connection, none should produce a status code
+    # reported by erki@example.ee
+    def test_101_05(self, env):
+        r = env.run([env.h2load, "-n", "10", "-c", "1", "-m", "1", "-vvvv",
+                     f"{env.https_base_url}/ssl-client-verify/index.html"])
+        assert 0 == r.exit_code
+        r = env.h2load_status(r)
+        assert 10 == r.results["h2load"]["requests"]["total"]
+        assert 10 == r.results["h2load"]["requests"]["started"]
+        assert 10 == r.results["h2load"]["requests"]["done"]
+        assert 0 == r.results["h2load"]["requests"]["succeeded"]
+        assert 0 == r.results["h2load"]["status"]["2xx"]
+        assert 0 == r.results["h2load"]["status"]["3xx"]
+        assert 0 == r.results["h2load"]["status"]["4xx"]
+        assert 0 == r.results["h2load"]["status"]["5xx"]
+
+    # Check that "SSLRequireSSL" works on h2 connections
+    # See <https://bz.apache.org/bugzilla/show_bug.cgi?id=62654>
+    def test_101_10a(self, env):
+        url = env.mkurl("https", "ssl", "/sslrequire/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert 404 == r.response["status"]
+
+    # Check that "require ssl" works on h2 connections
+    # See <https://bz.apache.org/bugzilla/show_bug.cgi?id=62654>
+    def test_101_10b(self, env):
+        url = env.mkurl("https", "ssl", "/requiressl/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert 404 == r.response["status"]
+        
+    # Check that status works with ErrorDoc, see pull #174, fixes #172
+    def test_101_11(self, env):
+        url = env.mkurl("https", "ssl", "/renegotiate/err-doc-cipher")
+        r = env.curl_get(url, options=[
+            "-vvv", "--tlsv1.2", "--tls-max", "1.2", "--ciphers", "ECDHE-RSA-AES256-GCM-SHA384"
+        ])
+        assert 0 != r.exit_code
+        assert not r.response
+        assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)

Added: httpd/httpd/trunk/test/modules/http2/test_102_require.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_102_require.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_102_require.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_102_require.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,38 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        conf = HttpdConf(env).start_vhost(env.https_port, "ssl", with_ssl=True)
+        conf.add("""
+              Protocols h2 http/1.1
+              SSLOptions +StdEnvVars
+              <Location /h2only.html>
+                Require expr \"%{HTTP2} == 'on'\"
+              </Location>
+              <Location /noh2.html>
+                Require expr \"%{HTTP2} == 'off'\"
+              </Location>""")
+        conf.end_vhost()
+        conf.install()
+        # the dir needs to exists for the configuration to have effect
+        env.mkpath("%s/htdocs/ssl-client-verify" % env.server_dir)
+        assert env.apache_restart() == 0
+
+    def test_102_01(self, env):
+        url = env.mkurl("https", "ssl", "/h2only.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert 404 == r.response["status"]
+        
+    def test_102_02(self, env):
+        url = env.mkurl("https", "ssl", "/noh2.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert 403 == r.response["status"]

Added: httpd/httpd/trunk/test/modules/http2/test_103_upgrade.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_103_upgrade.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_103_upgrade.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_103_upgrade.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,119 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_test1().add_vhost_test2().add_vhost_noh2(
+        ).start_vhost(
+            env.https_port, "test3", doc_root="htdocs/test1", with_ssl=True
+        ).add(
+            """
+            Protocols h2 http/1.1
+            Header unset Upgrade"""
+        ).end_vhost(
+        ).start_vhost(
+            env.http_port, "test1b", doc_root="htdocs/test1", with_ssl=False
+        ).add(
+            """
+            Protocols h2c http/1.1
+            H2Upgrade off
+            <Location /006.html>
+                H2Upgrade on
+            </Location>"""
+        ).end_vhost(
+        ).install()
+        assert env.apache_restart() == 0
+
+    # accessing http://test1, will not try h2 and advertise h2 in the response
+    def test_103_01(self, env):
+        url = env.mkurl("http", "test1", "/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" in r.response["header"]
+        assert "h2c" == r.response["header"]["upgrade"]
+        
+    # accessing http://noh2, will not advertise, because noh2 host does not have it enabled
+    def test_103_02(self, env):
+        url = env.mkurl("http", "noh2", "/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" not in r.response["header"]
+        
+    # accessing http://test2, will not advertise, because h2 has less preference than http/1.1
+    def test_103_03(self, env):
+        url = env.mkurl("http", "test2", "/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" not in r.response["header"]
+
+    # accessing https://noh2, will not advertise, because noh2 host does not have it enabled
+    def test_103_04(self, env):
+        url = env.mkurl("https", "noh2", "/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" not in r.response["header"]
+
+    # accessing https://test2, will not advertise, because h2 has less preference than http/1.1
+    def test_103_05(self, env):
+        url = env.mkurl("https", "test2", "/index.html")
+        r = env.curl_get(url)
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" not in r.response["header"]
+        
+    # accessing https://test1, will advertise h2 in the response
+    def test_103_06(self, env):
+        url = env.mkurl("https", "test1", "/index.html")
+        r = env.curl_get(url, options=["--http1.1"])
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" in r.response["header"]
+        assert "h2" == r.response["header"]["upgrade"]
+        
+    # accessing https://test3, will not send Upgrade since it is suppressed
+    def test_103_07(self, env):
+        url = env.mkurl("https", "test3", "/index.html")
+        r = env.curl_get(url, options=["--http1.1"])
+        assert 0 == r.exit_code
+        assert r.response
+        assert "upgrade" not in r.response["header"]
+
+    # upgrade to h2c for a request, where h2c is preferred
+    def test_103_20(self, env):
+        url = env.mkurl("http", "test1", "/index.html")
+        r = env.nghttp().get(url, options=["-u"])
+        assert 200 == r.response["status"]
+
+    # upgrade to h2c for a request where http/1.1 is preferred, but the clients upgrade
+    # wish is honored nevertheless
+    def test_103_21(self, env):
+        url = env.mkurl("http", "test2", "/index.html")
+        r = env.nghttp().get(url, options=["-u"])
+        assert 404 == r.response["status"]
+
+    # ugrade to h2c on a host where h2c is not enabled will fail
+    def test_103_22(self, env):
+        url = env.mkurl("http", "noh2", "/index.html")
+        r = env.nghttp().get(url, options=["-u"])
+        assert not r.response
+
+    # ugrade to h2c on a host where h2c is preferred, but Upgrade is disabled
+    def test_103_23(self, env):
+        url = env.mkurl("http", "test1b", "/index.html")
+        r = env.nghttp().get(url, options=["-u"])
+        assert not r.response
+
+    # ugrade to h2c on a host where h2c is preferred, but Upgrade is disabled on the server,
+    # but allowed for a specific location
+    def test_103_24(self, env):
+        url = env.mkurl("http", "test1b", "/006.html")
+        r = env.nghttp().get(url, options=["-u"])
+        assert 200 == r.response["status"]

Added: httpd/httpd/trunk/test/modules/http2/test_104_padding.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_104_padding.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_104_padding.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_104_padding.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,100 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+def frame_padding(payload, padbits):
+    mask = (1 << padbits) - 1
+    return ((payload + 9 + mask) & ~mask) - (payload + 9)
+        
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        conf = HttpdConf(env)
+        conf.add_vhost_cgi()
+        conf.start_vhost(env.https_port, "pad0", doc_root="htdocs/cgi", with_ssl=True)
+        conf.add("Protocols h2 http/1.1")
+        conf.add("H2Padding 0")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.start_vhost(env.https_port, "pad1", doc_root="htdocs/cgi", with_ssl=True)
+        conf.add("Protocols h2 http/1.1")
+        conf.add("H2Padding 1")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.start_vhost(env.https_port, "pad2", doc_root="htdocs/cgi", with_ssl=True)
+        conf.add("Protocols h2 http/1.1")
+        conf.add("H2Padding 2")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.start_vhost(env.https_port, "pad3", doc_root="htdocs/cgi", with_ssl=True)
+        conf.add("Protocols h2 http/1.1")
+        conf.add("H2Padding 3")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.start_vhost(env.https_port, "pad8", doc_root="htdocs/cgi", with_ssl=True)
+        conf.add("Protocols h2 http/1.1")
+        conf.add("H2Padding 8")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.install()
+        assert env.apache_restart() == 0
+
+    # default paddings settings: 0 bits
+    def test_104_01(self, env):
+        url = env.mkurl("https", "cgi", "/echo.py")
+        # we get 2 frames back: one with data and an empty one with EOF
+        # check the number of padding bytes is as expected
+        for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]:
+            r = env.nghttp().post_data(url, data, 5)
+            assert 200 == r.response["status"]
+            assert r.results["paddings"] == [
+                frame_padding(len(data)+1, 0), 
+                frame_padding(0, 0)
+            ]
+
+    # 0 bits of padding
+    def test_104_02(self, env):
+        url = env.mkurl("https", "pad0", "/echo.py")
+        for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]:
+            r = env.nghttp().post_data(url, data, 5)
+            assert 200 == r.response["status"]
+            assert r.results["paddings"] == [0, 0]
+
+    # 1 bit of padding
+    def test_104_03(self, env):
+        url = env.mkurl("https", "pad1", "/echo.py")
+        for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]:
+            r = env.nghttp().post_data(url, data, 5)
+            assert 200 == r.response["status"]
+            for i in r.results["paddings"]:
+                assert i in range(0, 2)
+
+    # 2 bits of padding
+    def test_104_04(self, env):
+        url = env.mkurl("https", "pad2", "/echo.py")
+        for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]:
+            r = env.nghttp().post_data(url, data, 5)
+            assert 200 == r.response["status"]
+            for i in r.results["paddings"]:
+                assert i in range(0, 4)
+
+    # 3 bits of padding
+    def test_104_05(self, env):
+        url = env.mkurl("https", "pad3", "/echo.py")
+        for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]:
+            r = env.nghttp().post_data(url, data, 5)
+            assert 200 == r.response["status"]
+            for i in r.results["paddings"]:
+                assert i in range(0, 8)
+
+    # 8 bits of padding
+    def test_104_06(self, env):
+        url = env.mkurl("https", "pad8", "/echo.py")
+        for data in ["x", "xx", "xxx", "xxxx", "xxxxx", "xxxxxx", "xxxxxxx", "xxxxxxxx"]:
+            r = env.nghttp().post_data(url, data, 5)
+            assert 200 == r.response["status"]
+            for i in r.results["paddings"]:
+                assert i in range(0, 256)

Added: httpd/httpd/trunk/test/modules/http2/test_105_timeout.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_105_timeout.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_105_timeout.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_105_timeout.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,96 @@
+import socket
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    # Check that base servers 'Timeout' setting is observed on SSL handshake
+    def test_105_01(self, env):
+        conf = HttpdConf(env)
+        conf.add("""
+            AcceptFilter http none
+            Timeout 1.5
+            """)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        host = 'localhost'
+        # read with a longer timeout than the server 
+        sock = socket.create_connection((host, int(env.https_port)))
+        try:
+            # on some OS, the server does not see our connection until there is
+            # something incoming
+            sock.send(b'0')
+            sock.settimeout(4)
+            buff = sock.recv(1024)
+            assert buff == b''
+        except Exception as ex:
+            print(f"server did not close in time: {ex}")
+            assert False
+        sock.close()
+        # read with a shorter timeout than the server 
+        sock = socket.create_connection((host, int(env.https_port)))
+        try:
+            sock.settimeout(0.5)
+            sock.recv(1024)
+            assert False
+        except Exception as ex:
+            print(f"as expected: {ex}")
+        sock.close()
+
+    # Check that mod_reqtimeout handshake setting takes effect
+    def test_105_02(self, env):
+        conf = HttpdConf(env)
+        conf.add("""
+            AcceptFilter http none
+            Timeout 10
+            RequestReadTimeout handshake=1 header=5 body=10
+            """)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        host = 'localhost'
+        # read with a longer timeout than the server 
+        sock = socket.create_connection((host, int(env.https_port)))
+        try:
+            # on some OS, the server does not see our connection until there is
+            # something incoming
+            sock.send(b'0')
+            sock.settimeout(4)
+            buff = sock.recv(1024)
+            assert buff == b''
+        except Exception as ex:
+            print(f"server did not close in time: {ex}")
+            assert False
+        sock.close()
+        # read with a shorter timeout than the server 
+        sock = socket.create_connection((host, int(env.https_port)))
+        try:
+            sock.settimeout(0.5)
+            sock.recv(1024)
+            assert False
+        except Exception as ex:
+            print(f"as expected: {ex}")
+        sock.close()
+
+    # Check that mod_reqtimeout handshake setting do no longer apply to handshaked 
+    # connections. See <https://github.com/icing/mod_h2/issues/196>.
+    def test_105_03(self, env):
+        conf = HttpdConf(env)
+        conf.add("""
+            Timeout 10
+            RequestReadTimeout handshake=1 header=5 body=10
+            """)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/necho.py")
+        r = env.curl_get(url, 5, [
+            "-vvv",
+            "-F", ("count=%d" % 100),
+            "-F", ("text=%s" % "abcdefghijklmnopqrstuvwxyz"),
+            "-F", ("wait1=%f" % 1.5),
+        ])
+        assert 200 == r.response["status"]

Added: httpd/httpd/trunk/test/modules/http2/test_106_shutdown.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_106_shutdown.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_106_shutdown.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_106_shutdown.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,45 @@
+#
+# mod-h2 test suite
+# check HTTP/2 timeout behaviour
+#
+import time
+from threading import Thread
+
+import pytest
+
+from h2_conf import HttpdConf
+from h2_result import ExecResult
+
+
+class TestShutdown:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        conf = HttpdConf(env)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+
+    def test_106_01(self, env):
+        url = env.mkurl("https", "cgi", "/necho.py")
+        lines = 100000
+        text = "123456789"
+        wait2 = 1.0
+        self.r = None
+        def long_request():
+            args = ["-vvv",
+                    "-F", f"count={lines}",
+                    "-F", f"text={text}",
+                    "-F", f"wait2={wait2}",
+                    ]
+            self.r = env.curl_get(url, 5, args)
+
+        t = Thread(target=long_request)
+        t.start()
+        time.sleep(0.5)
+        assert env.apache_restart() == 0
+        t.join()
+        # noinspection PyTypeChecker
+        r: ExecResult = self.r
+        assert r.response["status"] == 200
+        assert len(r.response["body"]) == (lines * (len(text)+1))

Added: httpd/httpd/trunk/test/modules/http2/test_200_header_invalid.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_200_header_invalid.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_200_header_invalid.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_200_header_invalid.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,172 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_cgi().install()
+        assert env.apache_restart() == 0
+
+    # let the hecho.py CGI echo chars < 0x20 in field name
+    # for almost all such characters, the stream gets aborted with a h2 error and 
+    # there will be no http status, cr and lf are handled special
+    def test_200_01(self, env):
+        url = env.mkurl("https", "cgi", "/hecho.py")
+        for x in range(1, 32):
+            r = env.curl_post_data(url, "name=x%%%02xx&value=yz" % x)
+            if x in [10]:
+                assert 0 == r.exit_code, "unexpected exit code for char 0x%02x" % x
+                assert 500 == r.response["status"], "unexpected status for char 0x%02x" % x
+            elif x in [13]:
+                assert 0 == r.exit_code, "unexpected exit code for char 0x%02x" % x
+                assert 200 == r.response["status"], "unexpected status for char 0x%02x" % x
+            else:
+                assert 0 != r.exit_code, "unexpected exit code for char 0x%02x" % x
+
+    # let the hecho.py CGI echo chars < 0x20 in field value
+    # for almost all such characters, the stream gets aborted with a h2 error and 
+    # there will be no http status, cr and lf are handled special
+    def test_200_02(self, env):
+        url = env.mkurl("https", "cgi", "/hecho.py")
+        for x in range(1, 32):
+            if 9 != x:
+                r = env.curl_post_data(url, "name=x&value=y%%%02x" % x)
+                if x in [10, 13]:
+                    assert 0 == r.exit_code, "unexpected exit code for char 0x%02x" % x
+                    assert 200 == r.response["status"], "unexpected status for char 0x%02x" % x
+                else:
+                    assert 0 != r.exit_code, "unexpected exit code for char 0x%02x" % x
+
+    # let the hecho.py CGI echo 0x10 and 0x7f in field name and value
+    def test_200_03(self, env):
+        url = env.mkurl("https", "cgi", "/hecho.py")
+        for h in ["10", "7f"]:
+            r = env.curl_post_data(url, "name=x%%%s&value=yz" % h)
+            assert 0 != r.exit_code
+            r = env.curl_post_data(url, "name=x&value=y%%%sz" % h)
+            assert 0 != r.exit_code
+    
+    # test header field lengths check, LimitRequestLine (default 8190)
+    def test_200_10(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        val = "1234567890"  # 10 chars
+        for i in range(3):  # make a 10000 char string
+            val = "%s%s%s%s%s%s%s%s%s%s" % (val, val, val, val, val, val, val, val, val, val)
+        # LimitRequestLine 8190 ok, one more char -> 431
+        r = env.curl_get(url, options=["-H", "x: %s" % (val[:8187])])
+        assert 200 == r.response["status"]
+        r = env.curl_get(url, options=["-H", "x: %sx" % (val[:8188])])
+        assert 431 == r.response["status"]
+
+    # test header field lengths check, LimitRequestFieldSize (default 8190)
+    def test_200_11(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        val = "1234567890"  # 10 chars
+        for i in range(3):  # make a 10000 char string
+            val = "%s%s%s%s%s%s%s%s%s%s" % (val, val, val, val, val, val, val, val, val, val)
+        # LimitRequestFieldSize 8190 ok, one more char -> 400 in HTTP/1.1
+        # (we send 4000+4185 since they are concatenated by ", " and start with "x: "
+        r = env.curl_get(url, options=["-H", "x: %s" % (val[:4000]),  "-H", "x: %s" % (val[:4185])])
+        assert 200 == r.response["status"]
+        r = env.curl_get(url, options=["--http1.1", "-H", "x: %s" % (val[:4000]),  "-H", "x: %s" % (val[:4189])])
+        assert 400 == r.response["status"]
+        r = env.curl_get(url, options=["-H", "x: %s" % (val[:4000]),  "-H", "x: %s" % (val[:4191])])
+        assert 431 == r.response["status"]
+
+    # test header field count, LimitRequestFields (default 100)
+    # see #201: several headers with same name are mered and count only once
+    def test_200_12(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        opt = []
+        for i in range(98):  # curl sends 2 headers itself (user-agent and accept)
+            opt += ["-H", "x: 1"]
+        r = env.curl_get(url, options=opt)
+        assert 200 == r.response["status"]
+        r = env.curl_get(url, options=(opt + ["-H", "y: 2"]))
+        assert 200 == r.response["status"]
+
+    # test header field count, LimitRequestFields (default 100)
+    # different header names count each
+    def test_200_13(self, env):
+        url = env.mkurl("https", "cgi", "/")
+        opt = []
+        for i in range(98):  # curl sends 2 headers itself (user-agent and accept)
+            opt += ["-H", "x{0}: 1".format(i)]
+        r = env.curl_get(url, options=opt)
+        assert 200 == r.response["status"]
+        r = env.curl_get(url, options=(opt + ["-H", "y: 2"]))
+        assert 431 == r.response["status"]
+
+    # test "LimitRequestFields 0" setting, see #200
+    def test_200_14(self, env):
+        conf = HttpdConf(env)
+        conf.add("""
+            LimitRequestFields 20
+            """)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/")
+        opt = []
+        for i in range(21):
+            opt += ["-H", "x{0}: 1".format(i)]
+        r = env.curl_get(url, options=opt)
+        assert 431 == r.response["status"]
+        conf = HttpdConf(env)
+        conf.add("""
+            LimitRequestFields 0
+            """)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/")
+        opt = []
+        for i in range(100):
+            opt += ["-H", "x{0}: 1".format(i)]
+        r = env.curl_get(url, options=opt)
+        assert 200 == r.response["status"]
+
+    # the uri limits
+    def test_200_15(self, env):
+        conf = HttpdConf(env)
+        conf.add("""
+            LimitRequestLine 48
+            """)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/")
+        r = env.curl_get(url)
+        assert 200 == r.response["status"]
+        url = env.mkurl("https", "cgi", "/" + (48*"x"))
+        r = env.curl_get(url)
+        assert 414 == r.response["status"]
+        # nghttp sends the :method: header first (so far)
+        # trigger a too long request line on it
+        # the stream will RST and we get no response
+        url = env.mkurl("https", "cgi", "/")
+        opt = ["-H:method: {0}".format(100*"x")]
+        r = env.nghttp().get(url, options=opt)
+        assert r.exit_code == 0, r
+        assert not r.response
+
+    # invalid chars in method
+    def test_200_16(self, env):
+        conf = HttpdConf(env)
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/hello.py")
+        opt = ["-H:method: GET /hello.py"]
+        r = env.nghttp().get(url, options=opt)
+        assert r.exit_code == 0, r
+        assert r.response
+        assert r.response["status"] == 400
+        url = env.mkurl("https", "cgi", "/proxy/hello.py")
+        r = env.nghttp().get(url, options=opt)
+        assert r.exit_code == 0, r
+        assert r.response
+        assert r.response["status"] == 400

Added: httpd/httpd/trunk/test/modules/http2/test_201_header_conditional.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_201_header_conditional.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_201_header_conditional.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_201_header_conditional.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,69 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add(
+            """
+            KeepAlive on
+            MaxKeepAliveRequests 30
+            KeepAliveTimeout 30"""
+        ).add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    # check handling of 'if-modified-since' header
+    def test_201_01(self, env):
+        url = env.mkurl("https", "test1", "/006/006.css")
+        r = env.curl_get(url)
+        assert 200 == r.response["status"]
+        lm = r.response["header"]["last-modified"]
+        assert lm
+        r = env.curl_get(url, options=["-H", "if-modified-since: %s" % lm])
+        assert 304 == r.response["status"]
+        r = env.curl_get(url, options=["-H", "if-modified-since: Tue, 04 Sep 2010 11:51:59 GMT"])
+        assert 200 == r.response["status"]
+
+    # check handling of 'if-none-match' header
+    def test_201_02(self, env):
+        url = env.mkurl("https", "test1", "/006/006.css")
+        r = env.curl_get(url)
+        assert 200 == r.response["status"]
+        etag = r.response["header"]["etag"]
+        assert etag
+        r = env.curl_get(url, options=["-H", "if-none-match: %s" % etag])
+        assert 304 == r.response["status"]
+        r = env.curl_get(url, options=["-H", "if-none-match: dummy"])
+        assert 200 == r.response["status"]
+        
+    @pytest.mark.skipif(True, reason="304 misses the Vary header in trunk and 2.4.x")
+    def test_201_03(self, env):
+        url = env.mkurl("https", "test1", "/006.html")
+        r = env.curl_get(url, options=["-H", "Accept-Encoding: gzip"])
+        assert 200 == r.response["status"]
+        for h in r.response["header"]:
+            print("%s: %s" % (h, r.response["header"][h]))
+        lm = r.response["header"]["last-modified"]
+        assert lm
+        assert "gzip" == r.response["header"]["content-encoding"]
+        assert "Accept-Encoding" in r.response["header"]["vary"]
+        
+        r = env.curl_get(url, options=["-H", "if-modified-since: %s" % lm,
+                                       "-H", "Accept-Encoding: gzip"])
+        assert 304 == r.response["status"]
+        for h in r.response["header"]:
+            print("%s: %s" % (h, r.response["header"][h]))
+        assert "vary" in r.response["header"]
+
+    # Check if "Keep-Alive" response header is removed in HTTP/2.
+    def test_201_04(self, env):
+        url = env.mkurl("https", "test1", "/006.html")
+        r = env.curl_get(url, options=["--http1.1", "-H", "Connection: keep-alive"])
+        assert 200 == r.response["status"]
+        assert "timeout=30, max=30" == r.response["header"]["keep-alive"]
+        r = env.curl_get(url, options=["-H", "Connection: keep-alive"])
+        assert 200 == r.response["status"]
+        assert "keep-alive" not in r.response["header"]

Added: httpd/httpd/trunk/test/modules/http2/test_202_trailer.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_202_trailer.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_202_trailer.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_202_trailer.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,81 @@
+import os
+import pytest
+
+from h2_conf import HttpdConf
+
+
+def setup_data(env):
+    s100 = "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n"
+    with open(os.path.join(env.gen_dir, "data-1k"), 'w') as f:
+        for i in range(10):
+            f.write(s100)
+
+
+# The trailer tests depend on "nghttp" as no other client seems to be able to send those
+# rare things.
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        setup_data(env)
+        HttpdConf(env).add_vhost_cgi(h2proxy_self=True).install()
+        assert env.apache_restart() == 0
+
+    # check if the server survives a trailer or two
+    def test_202_01(self, env):
+        url = env.mkurl("https", "cgi", "/echo.py")
+        fpath = os.path.join(env.gen_dir, "data-1k")
+        r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 1"])
+        assert 300 > r.response["status"]
+        assert 1000 == len(r.response["body"])
+
+        r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 1b", "--trailer", "XXX: test"])
+        assert 300 > r.response["status"]
+        assert 1000 == len(r.response["body"])
+
+    # check if the server survives a trailer without content-length
+    def test_202_02(self, env):
+        url = env.mkurl("https", "cgi", "/echo.py")
+        fpath = os.path.join(env.gen_dir, "data-1k")
+        r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 2", "--no-content-length"])
+        assert 300 > r.response["status"]
+        assert 1000 == len(r.response["body"])
+
+    # check if echoing request headers in response from GET works
+    def test_202_03(self, env):
+        url = env.mkurl("https", "cgi", "/echohd.py?name=X")
+        r = env.nghttp().get(url, options=["--header", "X: 3"])
+        assert 300 > r.response["status"]
+        assert b"X: 3\n" == r.response["body"]
+
+    # check if echoing request headers in response from POST works
+    def test_202_03b(self, env):
+        url = env.mkurl("https", "cgi", "/echohd.py?name=X")
+        r = env.nghttp().post_name(url, "Y", options=["--header", "X: 3b"])
+        assert 300 > r.response["status"]
+        assert b"X: 3b\n" == r.response["body"]
+
+    # check if echoing request headers in response from POST works, but trailers are not seen
+    # This is the way CGI invocation works.
+    def test_202_04(self, env):
+        url = env.mkurl("https", "cgi", "/echohd.py?name=X")
+        r = env.nghttp().post_name(url, "Y", options=["--header", "X: 4a", "--trailer", "X: 4b"])
+        assert 300 > r.response["status"]
+        assert b"X: 4a\n" == r.response["body"]
+
+    # The h2 status handler echoes a trailer if it sees a trailer
+    def test_202_05(self, env):
+        url = env.mkurl("https", "cgi", "/.well-known/h2/state")
+        fpath = os.path.join(env.gen_dir, "data-1k")
+        r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 2"])
+        assert 200 == r.response["status"]
+        assert "1" == r.response["trailer"]["h2-trailers-in"]
+
+    # Check that we can send and receive trailers throuh mod_proxy_http2
+    def test_202_06(self, env):
+        url = env.mkurl("https", "cgi", "/h2proxy/.well-known/h2/state")
+        fpath = os.path.join(env.gen_dir, "data-1k")
+        r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 2"])
+        assert 200 == r.response["status"]
+        assert 'trailer' in r.response
+        assert "1" == r.response['trailer']["h2-trailers-in"]

Added: httpd/httpd/trunk/test/modules/http2/test_300_interim.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_300_interim.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_300_interim.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_300_interim.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,39 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_test1().add_vhost_cgi().install()
+        assert env.apache_restart() == 0
+
+    def setup_method(self, method):
+        print("setup_method: %s" % method.__name__)
+
+    def teardown_method(self, method):
+        print("teardown_method: %s" % method.__name__)
+
+    # check that we normally do not see an interim response
+    def test_300_01(self, env):
+        url = env.mkurl("https", "test1", "/index.html")
+        r = env.curl_post_data(url, 'XYZ')
+        assert 200 == r.response["status"]
+        assert "previous" not in r.response
+
+    # check that we see an interim response when we ask for it
+    def test_300_02(self, env):
+        url = env.mkurl("https", "cgi", "/echo.py")
+        r = env.curl_post_data(url, 'XYZ', options=["-H", "expect: 100-continue"])
+        assert 200 == r.response["status"]
+        assert "previous" in r.response
+        assert 100 == r.response["previous"]["status"] 
+
+    # check proper answer on unexpected
+    def test_300_03(self, env):
+        url = env.mkurl("https", "cgi", "/echo.py")
+        r = env.curl_post_data(url, 'XYZ', options=["-H", "expect: the-unexpected"])
+        assert 417 == r.response["status"]
+        assert "previous" not in r.response

Added: httpd/httpd/trunk/test/modules/http2/test_400_push.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_400_push.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_400_push.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_400_push.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,199 @@
+import os
+import pytest
+
+from h2_conf import HttpdConf
+
+
+# The push tests depend on "nghttp"
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).start_vhost(
+            env.https_port, "push", doc_root="htdocs/test1", with_ssl=True
+        ).add(r"""    Protocols h2 http/1.1"
+        RewriteEngine on
+        RewriteRule ^/006-push(.*)?\.html$ /006.html
+        <Location /006-push.html>
+            Header add Link "</006/006.css>;rel=preload"
+            Header add Link "</006/006.js>;rel=preloadX"
+        </Location>
+        <Location /006-push2.html>
+            Header add Link "</006/006.css>;rel=preloadX, </006/006.js>; rel=preload"
+        </Location>
+        <Location /006-push3.html>
+            Header add Link "</006/006.css>;rel=preloa,</006/006.js>;rel=preload"
+        </Location>
+        <Location /006-push4.html>
+            Header add Link "</006/006.css;rel=preload, </006/006.js>; preload"
+        </Location>
+        <Location /006-push5.html>
+            Header add Link '</006/006.css>;rel="preload push"'
+        </Location>
+        <Location /006-push6.html>
+            Header add Link '</006/006.css>;rel="push preload"'
+        </Location>
+        <Location /006-push7.html>
+            Header add Link '</006/006.css>;rel="abc preload push"'
+        </Location>
+        <Location /006-push8.html>
+            Header add Link '</006/006.css>;rel="preload"; nopush'
+        </Location>
+        <Location /006-push20.html>
+            H2PushResource "/006/006.css" critical
+            H2PushResource "/006/006.js"
+        </Location>    
+        <Location /006-push30.html>
+            H2Push off
+            Header add Link '</006/006.css>;rel="preload"'
+        </Location>
+        <Location /006-push31.html>
+            H2PushResource "/006/006.css" critical
+        </Location>
+        <Location /006-push32.html>
+            Header add Link "</006/006.css>;rel=preload"
+        </Location>
+        """).end_vhost(
+        ).install()
+        assert env.apache_restart() == 0
+
+    ############################
+    # Link: header handling, various combinations
+
+    # plain resource without configured pushes 
+    def test_400_00(self, env):
+        url = env.mkurl("https", "push", "/006.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 0 == len(promises)
+
+    # 2 link headers configured, only 1 triggers push
+    def test_400_01(self, env):
+        url = env.mkurl("https", "push", "/006-push.html")
+        r = env.nghttp().get(url, options=["-Haccept-encoding: none"])
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.css' == promises[0]["request"]["header"][":path"]
+        assert 216 == len(promises[0]["response"]["body"])
+
+    # Same as 400_01, but with single header line configured
+    def test_400_02(self, env):
+        url = env.mkurl("https", "push", "/006-push2.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.js' == promises[0]["request"]["header"][":path"]
+
+    # 2 Links, only one with correct rel attribue
+    def test_400_03(self, env):
+        url = env.mkurl("https", "push", "/006-push3.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.js' == promises[0]["request"]["header"][":path"]
+
+    # Missing > in Link header, PUSH not triggered
+    def test_400_04(self, env):
+        url = env.mkurl("https", "push", "/006-push4.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 0 == len(promises)
+
+    # More than one value in "rel" parameter
+    def test_400_05(self, env):
+        url = env.mkurl("https", "push", "/006-push5.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.css' == promises[0]["request"]["header"][":path"]
+
+    # Another "rel" parameter variation
+    def test_400_06(self, env):
+        url = env.mkurl("https", "push", "/006-push6.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.css' == promises[0]["request"]["header"][":path"]
+
+    # Another "rel" parameter variation
+    def test_400_07(self, env):
+        url = env.mkurl("https", "push", "/006-push7.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.css' == promises[0]["request"]["header"][":path"]
+
+    # Pushable link header with "nopush" attribute
+    def test_400_08(self, env):
+        url = env.mkurl("https", "push", "/006-push8.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 0 == len(promises)
+
+    # 2 H2PushResource config trigger on GET, but not on POST
+    def test_400_20(self, env):
+        url = env.mkurl("https", "push", "/006-push20.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 2 == len(promises)
+
+        fpath = os.path.join(env.gen_dir, "data-400-20")
+        with open(fpath, 'w') as f:
+            f.write("test upload data")
+        r = env.nghttp().upload(url, fpath)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 0 == len(promises)
+    
+    # H2Push configured Off in location
+    def test_400_30(self, env):
+        url = env.mkurl("https", "push", "/006-push30.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 0 == len(promises)
+
+    # - suppress PUSH
+    def test_400_50(self, env):
+        url = env.mkurl("https", "push", "/006-push.html")
+        r = env.nghttp().get(url, options=['-H', 'accept-push-policy: none'])
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 0 == len(promises)
+
+    # - default pushes desired
+    def test_400_51(self, env):
+        url = env.mkurl("https", "push", "/006-push.html")
+        r = env.nghttp().get(url, options=['-H', 'accept-push-policy: default'])
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+
+    # - HEAD pushes desired
+    def test_400_52(self, env):
+        url = env.mkurl("https", "push", "/006-push.html")
+        r = env.nghttp().get(url, options=['-H', 'accept-push-policy: head'])
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert '/006/006.css' == promises[0]["request"]["header"][":path"]
+        assert b"" == promises[0]["response"]["body"]
+        assert 0 == len(promises[0]["response"]["body"])
+
+    # - fast-load pushes desired
+    def test_400_53(self, env):
+        url = env.mkurl("https", "push", "/006-push.html")
+        r = env.nghttp().get(url, options=['-H', 'accept-push-policy: fast-load'])
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)

Added: httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_401_early_hints.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,46 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+# The push tests depend on "nghttp"
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).start_vhost(
+            env.https_port, "hints", doc_root="htdocs/test1", with_ssl=True
+        ).add("""    Protocols h2 http/1.1"
+        H2EarlyHints on
+        RewriteEngine on
+        RewriteRule ^/006-(.*)?\\.html$ /006.html
+        <Location /006-hints.html>
+            H2PushResource "/006/006.css" critical
+        </Location>
+        <Location /006-nohints.html>
+            Header add Link "</006/006.css>;rel=preload"
+        </Location>
+        """).end_vhost(
+        ).install()
+        assert env.apache_restart() == 0
+
+    # H2EarlyHints enabled in general, check that it works for H2PushResource
+    def test_401_31(self, env):
+        url = env.mkurl("https", "hints", "/006-hints.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        early = r.response["previous"]
+        assert early
+        assert 103 == int(early["header"][":status"])
+        assert early["header"]["link"]
+
+    # H2EarlyHints enabled in general, but does not trigger on added response headers
+    def test_401_32(self, env):
+        url = env.mkurl("https", "hints", "/006-nohints.html")
+        r = env.nghttp().get(url)
+        assert 200 == r.response["status"]
+        promises = r.results["streams"][r.response["id"]]["promises"]
+        assert 1 == len(promises)
+        assert "previous" not in r.response

Added: httpd/httpd/trunk/test/modules/http2/test_500_proxy.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_500_proxy.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_500_proxy.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_500_proxy.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,119 @@
+import os
+import re
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        env.setup_data_1k_1m()
+        HttpdConf(env).add_vhost_cgi(proxy_self=True).install()
+        assert env.apache_restart() == 0
+
+    def setup_method(self, method):
+        print("setup_method: %s" % method.__name__)
+
+    def teardown_method(self, method):
+        print("teardown_method: %s" % method.__name__)
+
+    def test_500_01(self, env):
+        url = env.mkurl("https", "cgi", "/proxy/hello.py")
+        r = env.curl_get(url, 5)
+        assert 200 == r.response["status"]
+        assert "HTTP/1.1" == r.response["json"]["protocol"]
+        assert "" == r.response["json"]["https"]
+        assert "" == r.response["json"]["ssl_protocol"]
+        assert "" == r.response["json"]["h2"]
+        assert "" == r.response["json"]["h2push"]
+
+    # upload and GET again using curl, compare to original content
+    def curl_upload_and_verify(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/proxy/upload.py")
+        fpath = os.path.join(env.gen_dir, fname)
+        r = env.curl_upload(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert 200 <= r.response["status"] < 300
+
+        # why is the scheme wrong?
+        r2 = env.curl_get(re.sub(r'http:', 'https:', r.response["header"]["location"]))
+        assert r2.exit_code == 0
+        assert r2.response["status"] == 200
+        with open(env.test_src(fpath), mode='rb') as file:
+            src = file.read()
+        assert src == r2.response["body"]
+
+    def test_500_10(self, env):
+        self.curl_upload_and_verify(env, "data-1k", ["--http2"])
+        self.curl_upload_and_verify(env, "data-10k", ["--http2"])
+        self.curl_upload_and_verify(env, "data-100k", ["--http2"])
+        self.curl_upload_and_verify(env, "data-1m", ["--http2"])
+
+    # POST some data using nghttp and see it echo'ed properly back
+    def nghttp_post_and_verify(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/proxy/echo.py")
+        fpath = os.path.join(env.gen_dir, fname)
+        r = env.nghttp().upload(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert 200 <= r.response["status"] < 300
+        with open(env.test_src(fpath), mode='rb') as file:
+            src = file.read()
+        assert src == r.response["body"]
+
+    def test_500_20(self, env):
+        self.nghttp_post_and_verify(env, "data-1k", [])
+        self.nghttp_post_and_verify(env, "data-10k", [])
+        self.nghttp_post_and_verify(env, "data-100k", [])
+        self.nghttp_post_and_verify(env, "data-1m", [])
+
+    def test_500_21(self, env):
+        self.nghttp_post_and_verify(env, "data-1k", ["--no-content-length"])
+        self.nghttp_post_and_verify(env, "data-10k", ["--no-content-length"])
+        self.nghttp_post_and_verify(env, "data-100k", ["--no-content-length"])
+        self.nghttp_post_and_verify(env, "data-1m", ["--no-content-length"])
+
+    # upload and GET again using nghttp, compare to original content
+    def nghttp_upload_and_verify(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/proxy/upload.py")
+        fpath = os.path.join(env.gen_dir, fname)
+
+        r = env.nghttp().upload_file(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert 200 <= r.response["status"] < 300
+        assert r.response["header"]["location"]
+
+        # why is the scheme wrong?
+        r2 = env.nghttp().get(re.sub(r'http:', 'https:', r.response["header"]["location"]))
+        assert r2.exit_code == 0
+        assert r2.response["status"] == 200
+        with open(env.test_src(fpath), mode='rb') as file:
+            src = file.read()
+        assert src == r2.response["body"]
+
+    def test_500_22(self, env):
+        self.nghttp_upload_and_verify(env, "data-1k", [])
+        self.nghttp_upload_and_verify(env, "data-10k", [])
+        self.nghttp_upload_and_verify(env, "data-100k", [])
+        self.nghttp_upload_and_verify(env, "data-1m", [])
+
+    def test_500_23(self, env):
+        self.nghttp_upload_and_verify(env, "data-1k", ["--no-content-length"])
+        self.nghttp_upload_and_verify(env, "data-10k", ["--no-content-length"])
+        self.nghttp_upload_and_verify(env, "data-100k", ["--no-content-length"])
+        self.nghttp_upload_and_verify(env, "data-1m", ["--no-content-length"])
+
+    # upload using nghttp and check returned status
+    def nghttp_upload_stat(self, env, fname, options=None):
+        url = env.mkurl("https", "cgi", "/proxy/upload.py")
+        fpath = os.path.join(env.gen_dir, fname)
+
+        r = env.nghttp().upload_file(url, fpath, options=options)
+        assert r.exit_code == 0
+        assert 200 <= r.response["status"] < 300
+        assert r.response["header"]["location"]
+
+    def test_500_24(self, env):
+        for i in range(100):
+            self.nghttp_upload_stat(env, "data-1k", ["--no-content-length"])

Added: httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_600_h2proxy.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,27 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        env.setup_data_1k_1m()
+        conf = HttpdConf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.add("LogLevel proxy_http2:trace2")
+        conf.add("LogLevel proxy:trace2")
+        conf.install()
+        assert env.apache_restart() == 0
+
+    def test_600_01(self, env):
+        url = env.mkurl("https", "cgi", "/h2proxy/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert r.response["json"]["protocol"] == "HTTP/2.0"
+        assert r.response["json"]["https"] == "on"
+        assert r.response["json"]["ssl_protocol"] != ""
+        assert r.response["json"]["h2"] == "on"
+        assert r.response["json"]["h2push"] == "off"
+        assert r.response["json"]["host"] == f"cgi.{env.http_tld}"

Added: httpd/httpd/trunk/test/modules/http2/test_700_load_get.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_700_load_get.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_700_load_get.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_700_load_get.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,54 @@
+import pytest
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        HttpdConf(env).add_vhost_cgi().add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    def check_h2load_ok(self, env, r, n):
+        assert 0 == r.exit_code
+        r = env.h2load_status(r)
+        assert n == r.results["h2load"]["requests"]["total"]
+        assert n == r.results["h2load"]["requests"]["started"]
+        assert n == r.results["h2load"]["requests"]["done"]
+        assert n == r.results["h2load"]["requests"]["succeeded"]
+        assert n == r.results["h2load"]["status"]["2xx"]
+        assert 0 == r.results["h2load"]["status"]["3xx"]
+        assert 0 == r.results["h2load"]["status"]["4xx"]
+        assert 0 == r.results["h2load"]["status"]["5xx"]
+    
+    # test load on cgi script, single connection, different sizes
+    @pytest.mark.parametrize("start", [
+        1000, 80000
+    ])
+    def test_700_10(self, env, start):
+        text = "X"
+        chunk = 32
+        for n in range(0, 5):
+            args = [env.h2load, "-n", "%d" % chunk, "-c", "1", "-m", "10",
+                    f"--base-uri={env.https_base_url}"]
+            for i in range(0, chunk):
+                args.append(env.mkurl("https", "cgi", ("/mnot164.py?count=%d&text=%s" % (start+(n*chunk)+i, text))))
+            r = env.run(args)
+            self.check_h2load_ok(env, r, chunk)
+
+    # test load on cgi script, single connection
+    @pytest.mark.parametrize("conns", [
+        1, 2, 16, 32
+    ])
+    def test_700_11(self, env, conns):
+        text = "X"
+        start = 1200
+        chunk = 64
+        for n in range(0, 5):
+            args = [env.h2load, "-n", "%d" % chunk, "-c", "%d" % conns, "-m", "10",
+                    f"--base-uri={env.https_base_url}"]
+            for i in range(0, chunk):
+                args.append(env.mkurl("https", "cgi", ("/mnot164.py?count=%d&text=%s" % (start+(n*chunk)+i, text))))
+            r = env.run(args)
+            self.check_h2load_ok(env, r, chunk)

Added: httpd/httpd/trunk/test/modules/http2/test_710_load_post_static.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_710_load_post_static.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_710_load_post_static.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_710_load_post_static.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,62 @@
+import pytest
+import os
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        env.setup_data_1k_1m()
+        HttpdConf(env).add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    def check_h2load_ok(self, env, r, n):
+        assert 0 == r.exit_code
+        r = env.h2load_status(r)
+        assert n == r.results["h2load"]["requests"]["total"]
+        assert n == r.results["h2load"]["requests"]["started"]
+        assert n == r.results["h2load"]["requests"]["done"]
+        assert n == r.results["h2load"]["requests"]["succeeded"]
+        assert n == r.results["h2load"]["status"]["2xx"]
+        assert 0 == r.results["h2load"]["status"]["3xx"]
+        assert 0 == r.results["h2load"]["status"]["4xx"]
+        assert 0 == r.results["h2load"]["status"]["5xx"]
+    
+    # test POST on static file, slurped in by server
+    def test_710_00(self, env):
+        url = env.mkurl("https", "test1", "/index.html")
+        n = 10
+        m = 1
+        conn = 1
+        fname = "data-10k"
+        args = [env.h2load, "-n", "%d" % n, "-c", "%d" % conn, "-m", "%d" % m,
+                f"--base-uri={env.https_base_url}",
+                "-d", os.path.join(env.gen_dir, fname), url]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)
+
+    def test_710_01(self, env):
+        url = env.mkurl("https", "test1", "/index.html")
+        n = 1000
+        m = 100
+        conn = 1
+        fname = "data-1k"
+        args = [env.h2load, "-n", "%d" % n, "-c", "%d" % conn, "-m", "%d" % m,
+                f"--base-uri={env.https_base_url}",
+                "-d", os.path.join(env.gen_dir, fname), url]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)
+
+    def test_710_02(self, env):
+        url = env.mkurl("https", "test1", "/index.html")
+        n = 100
+        m = 50
+        conn = 1
+        fname = "data-100k"
+        args = [env.h2load, "-n", "%d" % n, "-c", "%d" % conn, "-m", "%d" % m,
+                f"--base-uri={env.https_base_url}",
+                "-d", os.path.join(env.gen_dir, fname), url]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)

Added: httpd/httpd/trunk/test/modules/http2/test_711_load_post_cgi.py
URL: http://svn.apache.org/viewvc/httpd/httpd/trunk/test/modules/http2/test_711_load_post_cgi.py?rev=1892476&view=auto
==============================================================================
--- httpd/httpd/trunk/test/modules/http2/test_711_load_post_cgi.py (added)
+++ httpd/httpd/trunk/test/modules/http2/test_711_load_post_cgi.py Fri Aug 20 16:07:44 2021
@@ -0,0 +1,70 @@
+import pytest
+import os
+
+from h2_conf import HttpdConf
+
+
+class TestStore:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        env.setup_data_1k_1m()
+        HttpdConf(env).add_vhost_cgi(proxy_self=True, h2proxy_self=True).install()
+        assert env.apache_restart() == 0
+
+    def check_h2load_ok(self, env, r, n):
+        assert 0 == r.exit_code
+        r = env.h2load_status(r)
+        assert n == r.results["h2load"]["requests"]["total"]
+        assert n == r.results["h2load"]["requests"]["started"]
+        assert n == r.results["h2load"]["requests"]["done"]
+        assert n == r.results["h2load"]["requests"]["succeeded"]
+        assert n == r.results["h2load"]["status"]["2xx"]
+        assert 0 == r.results["h2load"]["status"]["3xx"]
+        assert 0 == r.results["h2load"]["status"]["4xx"]
+        assert 0 == r.results["h2load"]["status"]["5xx"]
+    
+    # test POST on cgi, where input is read
+    def test_711_10(self, env):
+        url = env.mkurl("https", "test1", "/echo.py")
+        n = 100
+        m = 5
+        conn = 1
+        fname = "data-100k"
+        args = [
+            env.h2load, "-n", str(n), "-c", str(conn), "-m", str(m),
+            f"--base-uri={env.https_base_url}",
+            "-d", os.path.join(env.gen_dir, fname), url
+        ]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)
+
+    # test POST on cgi via http/1.1 proxy, where input is read
+    def test_711_11(self, env):
+        url = env.mkurl("https", "test1", "/proxy/echo.py")
+        n = 100
+        m = 5
+        conn = 1
+        fname = "data-100k"
+        args = [
+            env.h2load, "-n", str(n), "-c", str(conn), "-m", str(m),
+            f"--base-uri={env.https_base_url}",
+            "-d", os.path.join(env.gen_dir, fname), url
+        ]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)
+
+    # test POST on cgi via h2proxy, where input is read
+    def test_711_12(self, env):
+        url = env.mkurl("https", "test1", "/h2proxy/echo.py")
+        n = 100
+        m = 5
+        conn = 1
+        fname = "data-100k"
+        args = [
+            env.h2load, "-n", str(n), "-c", str(conn), "-m", str(m),
+            f"--base-uri={env.https_base_url}",
+            "-d", os.path.join(env.gen_dir, fname), url
+        ]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)