| import re | |
| import pytest | |
| from .env import H2Conf, H2TestEnv | |
| @pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here") | |
| @H2TestEnv.needs_nghttp() | |
| class TestInvalidHeaders: | |
| @pytest.fixture(autouse=True, scope='class') | |
| def _class_scope(self, env): | |
| H2Conf(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 returns a 500 | |
| # or in httpd >= 2.5.0 gets aborted with a h2 error | |
| # cr is handled special | |
| def test_h2_200_01(self, env): | |
| url = env.mkurl("https", "cgi", "/hecho.py") | |
| for x in range(1, 32): | |
| data = f'name=x%{x:02x}x&value=yz' | |
| r = env.curl_post_data(url, data) | |
| if x in [13]: | |
| assert 0 == r.exit_code, f'unexpected exit code for char 0x{x:02}' | |
| assert 200 == r.response["status"], f'unexpected status for char 0x{x:02}' | |
| elif x in [10] or env.httpd_is_at_least('2.5.0'): | |
| assert 0 == r.exit_code, f'unexpected exit code for char 0x{x:02}' | |
| assert 500 == r.response["status"], f'unexpected status for char 0x{x:02}' | |
| else: | |
| assert 0 != r.exit_code, f'unexpected exit code for char 0x{x:02}' | |
| # | |
| env.httpd_error_log.ignore_recent( | |
| lognos = [ | |
| "AH02429" # Response header name contains invalid characters | |
| ], | |
| matches = [ | |
| r'.*malformed header from script \'hecho.py\': Bad header: x.*' | |
| ] | |
| ) | |
| # let the hecho.py CGI echo chars < 0x20 in field value | |
| # for almost all such characters, the stream returns a 500 | |
| # or in httpd >= 2.5.0 gets aborted with a h2 error | |
| # cr and lf are handled special | |
| def test_h2_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 | |
| elif env.httpd_is_at_least('2.5.0'): | |
| assert 0 == r.exit_code, f'unexpected exit code for char 0x{x:02}' | |
| assert 500 == r.response["status"], f'unexpected status for char 0x{x:02}' | |
| else: | |
| assert 0 != r.exit_code, "unexpected exit code for char 0x%02x" % x | |
| # | |
| env.httpd_error_log.ignore_recent( | |
| lognos = [ | |
| "AH02430" # Response header value contains invalid characters | |
| ] | |
| ) | |
| # let the hecho.py CGI echo 0x10 and 0x7f in field name and value | |
| def test_h2_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) | |
| if env.httpd_is_at_least('2.5.0'): | |
| assert 0 == r.exit_code, f"unexpected exit code for char 0x{h:02}" | |
| assert 500 == r.response["status"], f"unexpected exit code for char 0x{h:02}" | |
| else: | |
| assert 0 != r.exit_code | |
| r = env.curl_post_data(url, "name=x&value=y%%%sz" % h) | |
| if env.httpd_is_at_least('2.5.0'): | |
| assert 0 == r.exit_code, f"unexpected exit code for char 0x{h:02}" | |
| assert 500 == r.response["status"], f"unexpected exit code for char 0x{h:02}" | |
| else: | |
| assert 0 != r.exit_code | |
| # | |
| env.httpd_error_log.ignore_recent( | |
| lognos = [ | |
| "AH02429", # Response header name contains invalid characters | |
| "AH02430" # Response header value contains invalid characters | |
| ] | |
| ) | |
| # test header field lengths check, LimitRequestLine | |
| def test_h2_200_10(self, env): | |
| conf = H2Conf(env) | |
| conf.add(""" | |
| LimitRequestLine 1024 | |
| """) | |
| conf.add_vhost_cgi() | |
| conf.install() | |
| assert env.apache_restart() == 0 | |
| val = 200*"1234567890" | |
| url = env.mkurl("https", "cgi", f'/?{val[:1022]}') | |
| r = env.curl_get(url) | |
| assert r.response["status"] == 200 | |
| url = env.mkurl("https", "cgi", f'/?{val[:1023]}') | |
| r = env.curl_get(url) | |
| # URI too long | |
| assert 414 == r.response["status"] | |
| # test header field lengths check, LimitRequestFieldSize (default 8190) | |
| def test_h2_200_11(self, env): | |
| conf = H2Conf(env) | |
| conf.add(""" | |
| LimitRequestFieldSize 1024 | |
| """) | |
| conf.add_vhost_cgi() | |
| conf.install() | |
| assert env.apache_restart() == 0 | |
| url = env.mkurl("https", "cgi", "/") | |
| val = 200*"1234567890" | |
| # two fields, concatenated with ', ' | |
| # LimitRequestFieldSize, one more char -> 400 in HTTP/1.1 | |
| r = env.curl_get(url, options=[ | |
| '-H', f'x: {val[:500]}', '-H', f'x: {val[:519]}' | |
| ]) | |
| assert r.exit_code == 0, f'{r}' | |
| assert r.response["status"] == 200, f'{r}' | |
| r = env.curl_get(url, options=[ | |
| '--http1.1', '-H', f'x: {val[:500]}', '-H', f'x: {val[:523]}' | |
| ]) | |
| assert 400 == r.response["status"] | |
| r = env.curl_get(url, options=[ | |
| '-H', f'x: {val[:500]}', '-H', f'x: {val[:520]}' | |
| ]) | |
| assert 431 == r.response["status"] | |
| # test header field count, LimitRequestFields (default 100) | |
| # see #201: several headers with same name are merged and counted | |
| def test_h2_200_12(self, env): | |
| url = env.mkurl("https", "cgi", "/") | |
| opt = [] | |
| # curl sends 3 headers itself (user-agent, accept, and our AP-Test-Name) | |
| for i in range(97): | |
| opt += ["-H", "x: 1"] | |
| r = env.curl_get(url, options=opt) | |
| assert r.response["status"] == 200 | |
| r = env.curl_get(url, options=(opt + ["-H", "y: 2"])) | |
| assert r.response["status"] == 431 | |
| # test header field count, LimitRequestFields (default 100) | |
| # different header names count each | |
| def test_h2_200_13(self, env): | |
| url = env.mkurl("https", "cgi", "/") | |
| opt = [] | |
| # curl sends 3 headers itself (user-agent, accept, and our AP-Test-Name) | |
| for i in range(97): | |
| opt += ["-H", f"x{i}: 1"] | |
| r = env.curl_get(url, options=opt) | |
| assert r.response["status"] == 200 | |
| r = env.curl_get(url, options=(opt + ["-H", "y: 2"])) | |
| assert 431 == r.response["status"] | |
| # test "LimitRequestFields 0" setting, see #200 | |
| def test_h2_200_14(self, env): | |
| conf = H2Conf(env) | |
| conf.add(""" | |
| LimitRequestFields 20 | |
| """) | |
| conf.add_vhost_cgi() | |
| conf.install() | |
| assert env.apache_restart() == 0 | |
| url = env.mkurl("https", "cgi", "/") | |
| for desc, opts in [ | |
| ("regular", [v for i in range(21) for v in ("-H", f"x{i}: 1")]), | |
| ("cookie", [v for i in range(21) for v in ("-H", f"cookie: c{i}=v{i}")]), | |
| ]: | |
| r = env.curl_get(url, options=opts) | |
| assert r.response["status"] == 431, f"{desc} header failed" | |
| conf = H2Conf(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 r.response["status"] == 200 | |
| # the uri limits | |
| def test_h2_200_15(self, env): | |
| conf = H2Conf(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 r.response["status"] == 200 | |
| 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_h2_200_16(self, env): | |
| # this drives nghttp, not h2load: gate on the version of the client | |
| # actually used, so an absent h2load does not skip a runnable test | |
| env.require(env.nghttp_is_at_least('1.45.0'), | |
| 'nghttp2 version too old', | |
| dep='nghttp >= 1.45.0', | |
| detected=f"nghttp {env.nghttp_version_str()}") | |
| conf = H2Conf(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 is None | |
| url = env.mkurl("https", "cgi", "/proxy/hello.py") | |
| r = env.nghttp().get(url, options=opt) | |
| assert r.exit_code == 0, r | |
| assert r.response is None | |
| # test few failed headers, should | |
| def test_h2_200_17(self, env): | |
| url = env.mkurl("https", "cgi", "/") | |
| # test few failed headers, should give response | |
| def test_h2_200_17(self, env): | |
| conf = H2Conf(env) | |
| conf.add(""" | |
| LimitRequestFieldSize 20 | |
| LogLevel http2:debug | |
| """) | |
| conf.add_vhost_cgi() | |
| conf.install() | |
| assert env.apache_restart() == 0 | |
| re_emitted = re.compile(r'.* (AH03401: .* shutdown,|' | |
| r'AH03066: .* FRAME\[GOAWAY.*) remote.emitted=1') | |
| url = env.mkurl("https", "cgi", "/") | |
| opt = [] | |
| for i in range(10): | |
| opt += ["-H", f"x{i}: 012345678901234567890123456789"] | |
| r = env.curl_get(url, options=opt) | |
| assert r.response | |
| assert r.response["status"] == 431 | |
| assert env.httpd_error_log.scan_recent(re_emitted) | |
| # test too many failed headers, should give RST | |
| def test_h2_200_18(self, env): | |
| conf = H2Conf(env) | |
| conf.add(""" | |
| LimitRequestFieldSize 20 | |
| LogLevel http2:debug | |
| """) | |
| conf.add_vhost_cgi() | |
| conf.install() | |
| assert env.apache_restart() == 0 | |
| re_emitted = re.compile(r'.* (AH03401: .* shutdown,|' | |
| r'AH03066: .* FRAME\[GOAWAY.*) remote.emitted=1') | |
| url = env.mkurl("https", "cgi", "/") | |
| opt = [] | |
| for i in range(100): | |
| opt += ["-H", f"x{i}: 012345678901234567890123456789"] | |
| r = env.curl_get(url, options=opt) | |
| assert r.response is None | |
| assert env.httpd_error_log.scan_recent(re_emitted) | |
| # test header 10 invalid headers, should trigger stream RST | |
| def test_h2_200_19(self, env): | |
| url = env.mkurl("https", "cgi", "/") | |
| opt = [] | |
| invalid = '\x7f' | |
| for i in range(10): | |
| opt += ["-H", f"x{i}: {invalid}"] | |
| r = env.curl_get(url, options=opt) | |
| assert r.response is None | |