diff options
Diffstat (limited to '')
47 files changed, 3879 insertions, 0 deletions
diff --git a/test/modules/http2/.gitignore b/test/modules/http2/.gitignore new file mode 100644 index 0000000..d68cd09 --- /dev/null +++ b/test/modules/http2/.gitignore @@ -0,0 +1,3 @@ +gen +config.ini +__pycache__ diff --git a/test/modules/http2/Makefile.in b/test/modules/http2/Makefile.in new file mode 100644 index 0000000..15d404d --- /dev/null +++ b/test/modules/http2/Makefile.in @@ -0,0 +1,20 @@ + +# no targets: we don't want to build anything by default. if you want the +# test programs, then "make test" +TARGETS = + +bin_PROGRAMS = + +PROGRAM_LDADD = $(EXTRA_LDFLAGS) $(PROGRAM_DEPENDENCIES) $(EXTRA_LIBS) +PROGRAM_DEPENDENCIES = \ + $(top_srcdir)/srclib/apr-util/libaprutil.la \ + $(top_srcdir)/srclib/apr/libapr.la + +include $(top_builddir)/build/rules.mk + +test: $(bin_PROGRAMS) + +# example for building a test proggie +# dbu_OBJECTS = dbu.lo +# dbu: $(dbu_OBJECTS) +# $(LINK) $(dbu_OBJECTS) $(PROGRAM_LDADD) diff --git a/test/modules/http2/__init__.py b/test/modules/http2/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/modules/http2/__init__.py diff --git a/test/modules/http2/conftest.py b/test/modules/http2/conftest.py new file mode 100644 index 0000000..55d0c3a --- /dev/null +++ b/test/modules/http2/conftest.py @@ -0,0 +1,40 @@ +import logging +import os + +import pytest +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), '../..')) + +from .env import H2TestEnv + + +def pytest_report_header(config, startdir): + env = H2TestEnv() + return f"mod_h2 [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]" + + +@pytest.fixture(scope="package") +def env(pytestconfig) -> H2TestEnv: + level = logging.INFO + console = logging.StreamHandler() + console.setLevel(level) + console.setFormatter(logging.Formatter('%(levelname)s: %(message)s')) + logging.getLogger('').addHandler(console) + logging.getLogger('').setLevel(level=level) + env = H2TestEnv(pytestconfig=pytestconfig) + env.setup_httpd() + env.apache_access_log_clear() + env.httpd_error_log.clear_log() + return env + + +@pytest.fixture(autouse=True, scope="package") +def _session_scope(env): + yield + assert env.apache_stop() == 0 + errors, warnings = env.httpd_error_log.get_missed() + assert (len(errors), len(warnings)) == (0, 0),\ + f"apache logged {len(errors)} errors and {len(warnings)} warnings: \n"\ + "{0}\n{1}\n".format("\n".join(errors), "\n".join(warnings)) + diff --git a/test/modules/http2/env.py b/test/modules/http2/env.py new file mode 100644 index 0000000..537d3bf --- /dev/null +++ b/test/modules/http2/env.py @@ -0,0 +1,168 @@ +import inspect +import logging +import os +import re +import subprocess +from typing import Dict, Any + +from pyhttpd.certs import CertificateSpec +from pyhttpd.conf import HttpdConf +from pyhttpd.env import HttpdTestEnv, HttpdTestSetup + +log = logging.getLogger(__name__) + + +class H2TestSetup(HttpdTestSetup): + + def __init__(self, env: 'HttpdTestEnv'): + super().__init__(env=env) + self.add_source_dir(os.path.dirname(inspect.getfile(H2TestSetup))) + self.add_modules(["http2", "proxy_http2", "cgid", "autoindex", "ssl"]) + + def make(self): + super().make() + self._add_h2test() + self._setup_data_1k_1m() + + def _add_h2test(self): + local_dir = os.path.dirname(inspect.getfile(H2TestSetup)) + p = subprocess.run([self.env.apxs, '-c', 'mod_h2test.c'], + capture_output=True, + cwd=os.path.join(local_dir, 'mod_h2test')) + rv = p.returncode + if rv != 0: + log.error(f"compiling md_h2test failed: {p.stderr}") + raise Exception(f"compiling md_h2test failed: {p.stderr}") + + modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf') + with open(modules_conf, 'a') as fd: + # load our test module which is not installed + fd.write(f"LoadModule h2test_module \"{local_dir}/mod_h2test/.libs/mod_h2test.so\"\n") + + def _setup_data_1k_1m(self): + s90 = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n" + with open(os.path.join(self.env.gen_dir, "data-1k"), 'w') as f: + for i in range(10): + f.write(f"{i:09d}-{s90}") + with open(os.path.join(self.env.gen_dir, "data-10k"), 'w') as f: + for i in range(100): + f.write(f"{i:09d}-{s90}") + with open(os.path.join(self.env.gen_dir, "data-100k"), 'w') as f: + for i in range(1000): + f.write(f"{i:09d}-{s90}") + with open(os.path.join(self.env.gen_dir, "data-1m"), 'w') as f: + for i in range(10000): + f.write(f"{i:09d}-{s90}") + + +class H2TestEnv(HttpdTestEnv): + + @classmethod + @property + def is_unsupported(cls): + mpm_module = f"mpm_{os.environ['MPM']}" if 'MPM' in os.environ else 'mpm_event' + return mpm_module == 'mpm_prefork' + + def __init__(self, pytestconfig=None): + super().__init__(pytestconfig=pytestconfig) + self.add_httpd_conf([ + "H2MinWorkers 1", + "H2MaxWorkers 64", + "Protocols h2 http/1.1 h2c", + ]) + self.add_httpd_log_modules(["http2", "proxy_http2", "h2test", "proxy", "proxy_http"]) + self.add_cert_specs([ + CertificateSpec(domains=[ + f"push.{self._http_tld}", + f"hints.{self._http_tld}", + f"ssl.{self._http_tld}", + f"pad0.{self._http_tld}", + f"pad1.{self._http_tld}", + f"pad2.{self._http_tld}", + f"pad3.{self._http_tld}", + f"pad8.{self._http_tld}", + ]), + CertificateSpec(domains=[f"noh2.{self.http_tld}"], key_type='rsa2048'), + ]) + + self.httpd_error_log.set_ignored_lognos([ + 'AH02032', + 'AH01276', + 'AH01630', + 'AH00135', + 'AH02261', # Re-negotiation handshake failed (our test_101) + 'AH03490', # scoreboard full, happens on limit tests + 'AH02429', # invalid chars in response header names, see test_h2_200 + 'AH02430', # invalid chars in response header values, see test_h2_200 + 'AH10373', # SSL errors on uncompleted handshakes, see test_h2_105 + 'AH01247', # mod_cgid sometimes freaks out on load tests + 'AH01110', # error by proxy reading response + 'AH10400', # warning that 'enablereuse' has not effect in certain configs test_h2_600 + ]) + self.httpd_error_log.add_ignored_patterns([ + re.compile(r'.*malformed header from script \'hecho.py\': Bad header: x.*'), + re.compile(r'.*:tls_post_process_client_hello:.*'), + # OSSL 3 dropped the function name from the error description. Use the code instead: + # 0A0000C1 = no shared cipher -- Too restrictive SSLCipherSuite or using DSA server certificate? + re.compile(r'.*SSL Library Error: error:0A0000C1:.*'), + re.compile(r'.*:tls_process_client_certificate:.*'), + # OSSL 3 dropped the function name from the error description. Use the code instead: + # 0A0000C7 = peer did not return a certificate -- No CAs known to server for verification? + re.compile(r'.*SSL Library Error: error:0A0000C7:.*'), + re.compile(r'.*have incompatible TLS configurations.'), + ]) + + def setup_httpd(self, setup: HttpdTestSetup = None): + super().setup_httpd(setup=H2TestSetup(env=self)) + + +class H2Conf(HttpdConf): + + def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None): + super().__init__(env=env, extras=HttpdConf.merge_extras(extras, { + f"cgi.{env.http_tld}": [ + "SSLOptions +StdEnvVars", + "AddHandler cgi-script .py", + "<Location \"/h2test/echo\">", + " SetHandler h2test-echo", + "</Location>", + "<Location \"/h2test/delay\">", + " SetHandler h2test-delay", + "</Location>", + "<Location \"/h2test/error\">", + " SetHandler h2test-error", + "</Location>", + ] + })) + + def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=None, + ssl_module=None, with_certificates=None): + super().start_vhost(domains=domains, port=port, doc_root=doc_root, + with_ssl=with_ssl, ssl_module=ssl_module, + with_certificates=with_certificates) + if f"noh2.{self.env.http_tld}" in domains: + protos = ["http/1.1"] + elif port == self.env.https_port or with_ssl is True: + protos = ["h2", "http/1.1"] + else: + protos = ["h2c", "http/1.1"] + if f"test2.{self.env.http_tld}" in domains: + protos = reversed(protos) + self.add(f"Protocols {' '.join(protos)}") + return self + + def add_vhost_noh2(self): + domains = [f"noh2.{self.env.http_tld}", f"noh2-alias.{self.env.http_tld}"] + self.start_vhost(domains=domains, port=self.env.https_port, doc_root="htdocs/noh2") + self.add(["Protocols http/1.1", "SSLOptions +StdEnvVars"]) + self.end_vhost() + self.start_vhost(domains=domains, port=self.env.http_port, doc_root="htdocs/noh2") + self.add(["Protocols http/1.1", "SSLOptions +StdEnvVars"]) + self.end_vhost() + return self + + def add_vhost_test1(self, proxy_self=False, h2proxy_self=False): + return super().add_vhost_test1(proxy_self=proxy_self, h2proxy_self=h2proxy_self) + + def add_vhost_test2(self): + return super().add_vhost_test2() diff --git a/test/modules/http2/htdocs/cgi/alive.json b/test/modules/http2/htdocs/cgi/alive.json new file mode 100644 index 0000000..defe2c2 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/alive.json @@ -0,0 +1,4 @@ +{ + "host" : "cgi", + "alive" : true +} diff --git a/test/modules/http2/htdocs/cgi/echo.py b/test/modules/http2/htdocs/cgi/echo.py new file mode 100644 index 0000000..c9083e1 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/echo.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +import os, sys +import multipart + +status = '200 Ok' + +content = '' +for line in sys.stdin: + content += line + +# Just echo what we get +print("Status: 200") +print(f"Request-Length: {len(content)}") +print("Content-Type: application/data\n") +sys.stdout.write(content) + diff --git a/test/modules/http2/htdocs/cgi/echohd.py b/test/modules/http2/htdocs/cgi/echohd.py new file mode 100644 index 0000000..2a138cd --- /dev/null +++ b/test/modules/http2/htdocs/cgi/echohd.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + if "REQUEST_URI" in os.environ: + qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query) + for name, values in qforms.items(): + oforms[name] = values[0] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + return oforms, ofiles + + +forms, files = get_request_params() +name = forms['name'] if 'name' in forms else None + +if name: + print("Status: 200") + print("""\ +Content-Type: text/plain\n""") + print("""%s: %s""" % (name, os.environ['HTTP_'+name])) +else: + print("Status: 400 Parameter Missing") + print("""\ +Content-Type: text/html\n +<html><body> +<p>No name was specified</p> +</body></html>""") + + diff --git a/test/modules/http2/htdocs/cgi/env.py b/test/modules/http2/htdocs/cgi/env.py new file mode 100644 index 0000000..3af5764 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/env.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + if "REQUEST_URI" in os.environ: + qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query) + for name, values in qforms.items(): + oforms[name] = values[0] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + return oforms, ofiles + + +forms, files = get_request_params() + +status = '200 Ok' + +try: + ename = forms['name'] + + # Test if the file was uploaded + if ename is not None: + val = os.environ[ename] if ename in os.environ else "" + print("Status: 200") + print("""\ +Content-Type: text/plain\n""") + print(f"{ename}={val}") + + else: + print("Status: 400 Parameter Missing") + print("""\ +Content-Type: text/html\n + <html><body> + <p>No name was specified: name</p> + </body></html>""") + +except KeyError: + print("Status: 200 Ok") + print("""\ +Content-Type: text/html\n + <html><body> + Echo <form method="POST" enctype="application/x-www-form-urlencoded"> + <input type="text" name="name"> + <button type="submit">submit</button></form> + </body></html>""") + pass + + + diff --git a/test/modules/http2/htdocs/cgi/files/empty.txt b/test/modules/http2/htdocs/cgi/files/empty.txt new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/files/empty.txt diff --git a/test/modules/http2/htdocs/cgi/hecho.py b/test/modules/http2/htdocs/cgi/hecho.py new file mode 100644 index 0000000..fb9e330 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/hecho.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + if "REQUEST_URI" in os.environ: + qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query) + for name, values in qforms.items(): + oforms[name] = values[0] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + return oforms, ofiles + + +forms, files = get_request_params() + +status = '200 Ok' + +try: + + # A nested FieldStorage instance holds the file + name = forms['name'] + value = '' + + try: + value = forms['value'] + except KeyError: + value = os.environ.get("HTTP_"+name, "unset") + + # Test if a value was given + if name: + print("Status: 200") + print("%s: %s" % (name, value,)) + print ("""\ +Content-Type: text/plain\n""") + + else: + print("Status: 400 Parameter Missing") + print("""\ +Content-Type: text/html\n + <html><body> + <p>No name and value was specified: %s %s</p> + </body></html>""" % (name, value)) + +except KeyError: + print("Status: 200 Ok") + print("""\ +Content-Type: text/html\n + <html><body> + Echo <form method="POST" enctype="application/x-www-form-urlencoded"> + <input type="text" name="name"> + <input type="text" name="value"> + <button type="submit">Echo</button></form> + </body></html>""") + pass + + diff --git a/test/modules/http2/htdocs/cgi/hello.py b/test/modules/http2/htdocs/cgi/hello.py new file mode 100644 index 0000000..20974bf --- /dev/null +++ b/test/modules/http2/htdocs/cgi/hello.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import os + +print("Content-Type: application/json") +print() +print("{") +print(" \"https\" : \"%s\"," % (os.getenv('HTTPS', ''))) +print(" \"host\" : \"%s\"," % (os.getenv('X_HOST', '') \ + if 'X_HOST' in os.environ else os.getenv('SERVER_NAME', ''))) +print(" \"server\" : \"%s\"," % (os.getenv('SERVER_NAME', ''))) +print(" \"h2_original_host\" : \"%s\"," % (os.getenv('H2_ORIGINAL_HOST', ''))) +print(" \"port\" : \"%s\"," % (os.getenv('SERVER_PORT', ''))) +print(" \"protocol\" : \"%s\"," % (os.getenv('SERVER_PROTOCOL', ''))) +print(" \"ssl_protocol\" : \"%s\"," % (os.getenv('SSL_PROTOCOL', ''))) +print(" \"h2\" : \"%s\"," % (os.getenv('HTTP2', ''))) +print(" \"h2push\" : \"%s\"," % (os.getenv('H2PUSH', ''))) +print(" \"h2_stream_id\" : \"%s\"" % (os.getenv('H2_STREAM_ID', ''))) +print("}") + diff --git a/test/modules/http2/htdocs/cgi/mnot164.py b/test/modules/http2/htdocs/cgi/mnot164.py new file mode 100644 index 0000000..c29ccc1 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/mnot164.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + if "REQUEST_URI" in os.environ: + qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query) + for name, values in qforms.items(): + oforms[name] = values[0] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + return oforms, ofiles + + +forms, files = get_request_params() +text = forms['text'] if 'text' in forms else "a" +count = int(forms['count']) if 'count' in forms else 77784 + +print("Status: 200 OK") +print("Content-Type: text/html") +print() +sys.stdout.flush() +for _ in range(count): + sys.stdout.write(text) + diff --git a/test/modules/http2/htdocs/cgi/necho.py b/test/modules/http2/htdocs/cgi/necho.py new file mode 100644 index 0000000..78e2aad --- /dev/null +++ b/test/modules/http2/htdocs/cgi/necho.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +import time +import os, sys +import multipart +from urllib import parse + + +def get_request_params(): + oforms = {} + if "REQUEST_URI" in os.environ: + qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query) + for name, values in qforms.items(): + oforms[name] = values[0] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + return oforms, ofiles + + +forms, files = get_request_params() +status = '200 Ok' + +try: + count = forms['count'] + text = forms['text'] + + waitsec = float(forms['wait1']) if 'wait1' in forms else 0.0 + if waitsec > 0: + time.sleep(waitsec) + + if int(count): + print("Status: 200") + print("""\ +Content-Type: text/plain\n""") + + waitsec = float(forms['wait2']) if 'wait2' in forms else 0.0 + if waitsec > 0: + time.sleep(waitsec) + + i = 0; + for i in range(0, int(count)): + print("%s" % (text)) + + waitsec = float(forms['wait3']) if 'wait3' in forms else 0.0 + if waitsec > 0: + time.sleep(waitsec) + + else: + print("Status: 400 Parameter Missing") + print("""\ +Content-Type: text/html\n + <html><body> + <p>No count was specified: %s</p> + </body></html>""" % (count)) + +except KeyError: + print("Status: 200 Ok") + print("""\ +Content-Type: text/html\n + <html><body> + Echo <form method="POST" enctype="application/x-www-form-urlencoded"> + <input type="text" name="count"> + <input type="text" name="text"> + <button type="submit">Echo</button></form> + </body></html>""") + pass + + diff --git a/test/modules/http2/htdocs/cgi/upload.py b/test/modules/http2/htdocs/cgi/upload.py new file mode 100644 index 0000000..59fbb58 --- /dev/null +++ b/test/modules/http2/htdocs/cgi/upload.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +import os +import sys +import multipart +from urllib import parse + + +try: # Windows needs stdio set for binary mode. + import msvcrt + + msvcrt.setmode(0, os.O_BINARY) # stdin = 0 + msvcrt.setmode(1, os.O_BINARY) # stdout = 1 +except ImportError: + pass + +def get_request_params(): + oforms = {} + if "REQUEST_URI" in os.environ: + qforms = parse.parse_qs(parse.urlsplit(os.environ["REQUEST_URI"]).query) + for name, values in qforms.items(): + oforms[name] = values[0] + myenv = os.environ.copy() + myenv['wsgi.input'] = sys.stdin.buffer + mforms, ofiles = multipart.parse_form_data(environ=myenv) + for name, item in mforms.items(): + oforms[name] = item + return oforms, ofiles + + +forms, files = get_request_params() + +status = '200 Ok' + +# Test if the file was uploaded +if 'file' in files: + fitem = files['file'] + # strip leading path from file name to avoid directory traversal attacks + fname = fitem.filename + fpath = f'{os.environ["DOCUMENT_ROOT"]}/files/{fname}' + fitem.save_as(fpath) + message = "The file %s was uploaded successfully" % (fname) + print("Status: 201 Created") + print("Content-Type: text/html") + print("Location: %s://%s/files/%s" % (os.environ["REQUEST_SCHEME"], os.environ["HTTP_HOST"], fname)) + print("") + print("<html><body><p>%s</p></body></html>" % (message)) + +elif 'remove' in forms: + remove = forms['remove'] + try: + fname = os.path.basename(remove) + os.remove('./files/' + fname) + message = 'The file "' + fname + '" was removed successfully' + except OSError as e: + message = 'Error removing ' + fname + ': ' + e.strerror + status = '404 File Not Found' + print("Status: %s" % (status)) + print(""" +Content-Type: text/html + +<html><body> +<p>%s</p> +</body></html>""" % (message)) + +else: + message = '''\ + Upload File<form method="POST" enctype="multipart/form-data"> + <input type="file" name="file"> + <button type="submit">Upload</button></form> + ''' + print("Status: %s" % (status)) + print("""\ +Content-Type: text/html + +<html><body> +<p>%s</p> +</body></html>""" % (message)) + diff --git a/test/modules/http2/htdocs/noh2/alive.json b/test/modules/http2/htdocs/noh2/alive.json new file mode 100644 index 0000000..7b54893 --- /dev/null +++ b/test/modules/http2/htdocs/noh2/alive.json @@ -0,0 +1,5 @@ +{ + "host" : "noh2", + "alive" : true +} + diff --git a/test/modules/http2/htdocs/noh2/index.html b/test/modules/http2/htdocs/noh2/index.html new file mode 100644 index 0000000..696068e --- /dev/null +++ b/test/modules/http2/htdocs/noh2/index.html @@ -0,0 +1,9 @@ +<html> + <head> + <title>mod_h2 test site noh2</title> + </head> + <body> + <h1>mod_h2 test site noh2</h1> + </body> +</html> + diff --git a/test/modules/http2/mod_h2test/mod_h2test.c b/test/modules/http2/mod_h2test/mod_h2test.c new file mode 100644 index 0000000..b5ee8ad --- /dev/null +++ b/test/modules/http2/mod_h2test/mod_h2test.c @@ -0,0 +1,527 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <apr_optional.h> +#include <apr_optional_hooks.h> +#include <apr_strings.h> +#include <apr_cstr.h> +#include <apr_time.h> +#include <apr_want.h> + +#include <httpd.h> +#include <http_protocol.h> +#include <http_request.h> +#include <http_log.h> + +#include "mod_h2test.h" + +static void h2test_hooks(apr_pool_t *pool); + +AP_DECLARE_MODULE(h2test) = { + STANDARD20_MODULE_STUFF, + NULL, /* func to create per dir config */ + NULL, /* func to merge per dir config */ + NULL, /* func to create per server config */ + NULL, /* func to merge per server config */ + NULL, /* command handlers */ + h2test_hooks, +#if defined(AP_MODULE_FLAG_NONE) + AP_MODULE_FLAG_ALWAYS_MERGE +#endif +}; + +#define SECS_PER_HOUR (60*60) +#define SECS_PER_DAY (24*SECS_PER_HOUR) + +static apr_status_t duration_parse(apr_interval_time_t *ptimeout, const char *value, + const char *def_unit) +{ + char *endp; + apr_int64_t n; + + n = apr_strtoi64(value, &endp, 10); + if (errno) { + return errno; + } + if (!endp || !*endp) { + if (!def_unit) def_unit = "s"; + } + else if (endp == value) { + return APR_EINVAL; + } + else { + def_unit = endp; + } + + switch (*def_unit) { + case 'D': + case 'd': + *ptimeout = apr_time_from_sec(n * SECS_PER_DAY); + break; + case 's': + case 'S': + *ptimeout = (apr_interval_time_t) apr_time_from_sec(n); + break; + case 'h': + case 'H': + /* Time is in hours */ + *ptimeout = (apr_interval_time_t) apr_time_from_sec(n * SECS_PER_HOUR); + break; + case 'm': + case 'M': + switch (*(++def_unit)) { + /* Time is in milliseconds */ + case 's': + case 'S': + *ptimeout = (apr_interval_time_t) n * 1000; + break; + /* Time is in minutes */ + case 'i': + case 'I': + *ptimeout = (apr_interval_time_t) apr_time_from_sec(n * 60); + break; + default: + return APR_EGENERAL; + } + break; + default: + return APR_EGENERAL; + } + return APR_SUCCESS; +} + +static int h2test_post_config(apr_pool_t *p, apr_pool_t *plog, + apr_pool_t *ptemp, server_rec *s) +{ + void *data = NULL; + const char *mod_h2_init_key = "mod_h2test_init_counter"; + + (void)plog;(void)ptemp; + + apr_pool_userdata_get(&data, mod_h2_init_key, s->process->pool); + if ( data == NULL ) { + /* dry run */ + apr_pool_userdata_set((const void *)1, mod_h2_init_key, + apr_pool_cleanup_null, s->process->pool); + return APR_SUCCESS; + } + + + return APR_SUCCESS; +} + +static void h2test_child_init(apr_pool_t *pool, server_rec *s) +{ + (void)pool; + (void)s; +} + +static int h2test_echo_handler(request_rec *r) +{ + conn_rec *c = r->connection; + apr_bucket_brigade *bb; + apr_bucket *b; + apr_status_t rv; + char buffer[8192]; + const char *ct; + long l; + + if (strcmp(r->handler, "h2test-echo")) { + return DECLINED; + } + if (r->method_number != M_GET && r->method_number != M_POST) { + return DECLINED; + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "echo_handler: processing request"); + r->status = 200; + r->clength = -1; + r->chunked = 1; + apr_table_unset(r->headers_out, "Content-Length"); + /* Discourage content-encodings */ + apr_table_unset(r->headers_out, "Content-Encoding"); + apr_table_setn(r->subprocess_env, "no-brotli", "1"); + apr_table_setn(r->subprocess_env, "no-gzip", "1"); + + ct = apr_table_get(r->headers_in, "content-type"); + ap_set_content_type(r, ct? ct : "application/octet-stream"); + + bb = apr_brigade_create(r->pool, c->bucket_alloc); + /* copy any request body into the response */ + if ((rv = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK))) goto cleanup; + if (ap_should_client_block(r)) { + while (0 < (l = ap_get_client_block(r, &buffer[0], sizeof(buffer)))) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "echo_handler: copying %ld bytes from request body", l); + rv = apr_brigade_write(bb, NULL, NULL, buffer, l); + if (APR_SUCCESS != rv) goto cleanup; + rv = ap_pass_brigade(r->output_filters, bb); + if (APR_SUCCESS != rv) goto cleanup; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "echo_handler: passed %ld bytes from request body", l); + } + } + /* we are done */ + b = apr_bucket_eos_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "echo_handler: request read"); + + if (r->trailers_in && !apr_is_empty_table(r->trailers_in)) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r, + "echo_handler: seeing incoming trailers"); + apr_table_setn(r->trailers_out, "h2test-trailers-in", + apr_itoa(r->pool, 1)); + } + + rv = ap_pass_brigade(r->output_filters, bb); + +cleanup: + if (rv == APR_SUCCESS + || r->status != HTTP_OK + || c->aborted) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "echo_handler: request handled"); + return OK; + } + else { + /* no way to know what type of error occurred */ + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "h2test_echo_handler failed"); + return AP_FILTER_ERROR; + } + return DECLINED; +} + +static int h2test_delay_handler(request_rec *r) +{ + conn_rec *c = r->connection; + apr_bucket_brigade *bb; + apr_bucket *b; + apr_status_t rv; + char buffer[8192]; + int i, chunks = 3; + long l; + apr_time_t delay = 0; + + if (strcmp(r->handler, "h2test-delay")) { + return DECLINED; + } + if (r->method_number != M_GET && r->method_number != M_POST) { + return DECLINED; + } + + if (r->args) { + rv = duration_parse(&delay, r->args, "s"); + if (APR_SUCCESS != rv) { + ap_die(HTTP_BAD_REQUEST, r); + return OK; + } + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "delay_handler: processing request, %ds delay", + (int)apr_time_sec(delay)); + r->status = 200; + r->clength = -1; + r->chunked = 1; + apr_table_unset(r->headers_out, "Content-Length"); + /* Discourage content-encodings */ + apr_table_unset(r->headers_out, "Content-Encoding"); + apr_table_setn(r->subprocess_env, "no-brotli", "1"); + apr_table_setn(r->subprocess_env, "no-gzip", "1"); + + ap_set_content_type(r, "application/octet-stream"); + + bb = apr_brigade_create(r->pool, c->bucket_alloc); + /* copy any request body into the response */ + if ((rv = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK))) goto cleanup; + if (ap_should_client_block(r)) { + do { + l = ap_get_client_block(r, &buffer[0], sizeof(buffer)); + if (l > 0) { + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "delay_handler: reading %ld bytes from request body", l); + } + } while (l > 0); + if (l < 0) { + return AP_FILTER_ERROR; + } + } + + memset(buffer, 0, sizeof(buffer)); + l = sizeof(buffer); + for (i = 0; i < chunks; ++i) { + rv = apr_brigade_write(bb, NULL, NULL, buffer, l); + if (APR_SUCCESS != rv) goto cleanup; + rv = ap_pass_brigade(r->output_filters, bb); + if (APR_SUCCESS != rv) goto cleanup; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "delay_handler: passed %ld bytes as response body", l); + if (delay) { + apr_sleep(delay); + } + } + /* we are done */ + b = apr_bucket_eos_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + rv = ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "delay_handler: response passed"); + +cleanup: + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, + "delay_handler: request cleanup, r->status=%d, aborte=%d", + r->status, c->aborted); + if (rv == APR_SUCCESS + || r->status != HTTP_OK + || c->aborted) { + return OK; + } + return AP_FILTER_ERROR; +} + +static int h2test_trailer_handler(request_rec *r) +{ + conn_rec *c = r->connection; + apr_bucket_brigade *bb; + apr_bucket *b; + apr_status_t rv; + char buffer[8192]; + long l; + int body_len = 0; + + if (strcmp(r->handler, "h2test-trailer")) { + return DECLINED; + } + if (r->method_number != M_GET && r->method_number != M_POST) { + return DECLINED; + } + + if (r->args) { + body_len = (int)apr_atoi64(r->args); + if (body_len < 0) body_len = 0; + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "trailer_handler: processing request, %d body length", + body_len); + r->status = 200; + r->clength = body_len; + ap_set_content_length(r, body_len); + + ap_set_content_type(r, "application/octet-stream"); + apr_table_mergen(r->headers_out, "Trailer", "trailer-content-length"); + apr_table_set(r->trailers_out, "trailer-content-length", + apr_psprintf(r->pool, "%d", body_len)); + + bb = apr_brigade_create(r->pool, c->bucket_alloc); + memset(buffer, 0, sizeof(buffer)); + while (body_len > 0) { + l = (sizeof(buffer) > body_len)? body_len : sizeof(buffer); + body_len -= l; + rv = apr_brigade_write(bb, NULL, NULL, buffer, l); + if (APR_SUCCESS != rv) goto cleanup; + rv = ap_pass_brigade(r->output_filters, bb); + if (APR_SUCCESS != rv) goto cleanup; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "trailer_handler: passed %ld bytes as response body", l); + } + /* we are done */ + b = apr_bucket_eos_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + rv = ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "trailer_handler: response passed"); + +cleanup: + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, + "trailer_handler: request cleanup, r->status=%d, aborte=%d", + r->status, c->aborted); + if (rv == APR_SUCCESS + || r->status != HTTP_OK + || c->aborted) { + return OK; + } + return AP_FILTER_ERROR; +} + +static int status_from_str(const char *s, apr_status_t *pstatus) +{ + if (!strcmp("timeout", s)) { + *pstatus = APR_TIMEUP; + return 1; + } + else if (!strcmp("reset", s)) { + *pstatus = APR_ECONNRESET; + return 1; + } + return 0; +} + +static int h2test_error_handler(request_rec *r) +{ + conn_rec *c = r->connection; + apr_bucket_brigade *bb; + apr_bucket *b; + apr_status_t rv; + char buffer[8192]; + int i, chunks = 3, error_bucket = 1; + long l; + apr_time_t delay = 0, body_delay = 0; + apr_array_header_t *args = NULL; + int http_status = 200; + apr_status_t error = APR_SUCCESS, body_error = APR_SUCCESS; + + if (strcmp(r->handler, "h2test-error")) { + return DECLINED; + } + if (r->method_number != M_GET && r->method_number != M_POST) { + return DECLINED; + } + + if (r->args) { + args = apr_cstr_split(r->args, "&", 1, r->pool); + for (i = 0; i < args->nelts; ++i) { + char *s, *val, *arg = APR_ARRAY_IDX(args, i, char*); + s = strchr(arg, '='); + if (s) { + *s = '\0'; + val = s + 1; + if (!strcmp("status", arg)) { + http_status = (int)apr_atoi64(val); + if (val > 0) { + continue; + } + } + else if (!strcmp("error", arg)) { + if (status_from_str(val, &error)) { + continue; + } + } + else if (!strcmp("error_bucket", arg)) { + error_bucket = (int)apr_atoi64(val); + if (val >= 0) { + continue; + } + } + else if (!strcmp("body_error", arg)) { + if (status_from_str(val, &body_error)) { + continue; + } + } + else if (!strcmp("delay", arg)) { + rv = duration_parse(&delay, r->args, "s"); + if (APR_SUCCESS == rv) { + continue; + } + } + else if (!strcmp("body_delay", arg)) { + rv = duration_parse(&body_delay, r->args, "s"); + if (APR_SUCCESS == rv) { + continue; + } + } + } + ap_die(HTTP_BAD_REQUEST, r); + return OK; + } + } + + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, "error_handler: processing request, %s", + r->args? r->args : "(no args)"); + r->status = http_status; + r->clength = -1; + r->chunked = 1; + apr_table_unset(r->headers_out, "Content-Length"); + /* Discourage content-encodings */ + apr_table_unset(r->headers_out, "Content-Encoding"); + apr_table_setn(r->subprocess_env, "no-brotli", "1"); + apr_table_setn(r->subprocess_env, "no-gzip", "1"); + + ap_set_content_type(r, "application/octet-stream"); + bb = apr_brigade_create(r->pool, c->bucket_alloc); + + if (delay) { + apr_sleep(delay); + } + if (error != APR_SUCCESS) { + return ap_map_http_request_error(error, HTTP_BAD_REQUEST); + } + /* flush response */ + b = apr_bucket_flush_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + rv = ap_pass_brigade(r->output_filters, bb); + if (APR_SUCCESS != rv) goto cleanup; + + memset(buffer, 'X', sizeof(buffer)); + l = sizeof(buffer); + for (i = 0; i < chunks; ++i) { + if (body_delay) { + apr_sleep(body_delay); + } + rv = apr_brigade_write(bb, NULL, NULL, buffer, l); + if (APR_SUCCESS != rv) goto cleanup; + rv = ap_pass_brigade(r->output_filters, bb); + if (APR_SUCCESS != rv) goto cleanup; + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, 0, r, + "error_handler: passed %ld bytes as response body", l); + if (body_error != APR_SUCCESS) { + rv = body_error; + goto cleanup; + } + } + /* we are done */ + b = apr_bucket_eos_create(c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + rv = ap_pass_brigade(r->output_filters, bb); + apr_brigade_cleanup(bb); + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, "error_handler: response passed"); + +cleanup: + ap_log_rerror(APLOG_MARK, APLOG_TRACE1, rv, r, + "error_handler: request cleanup, r->status=%d, aborted=%d", + r->status, c->aborted); + if (rv == APR_SUCCESS) { + return OK; + } + if (error_bucket) { + http_status = ap_map_http_request_error(rv, HTTP_BAD_REQUEST); + b = ap_bucket_error_create(http_status, NULL, r->pool, c->bucket_alloc); + APR_BRIGADE_INSERT_TAIL(bb, b); + ap_pass_brigade(r->output_filters, bb); + } + return AP_FILTER_ERROR; +} + +/* Install this module into the apache2 infrastructure. + */ +static void h2test_hooks(apr_pool_t *pool) +{ + static const char *const mod_h2[] = { "mod_h2.c", NULL}; + + ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks and handlers"); + + /* Run once after configuration is set, but before mpm children initialize. + */ + ap_hook_post_config(h2test_post_config, mod_h2, NULL, APR_HOOK_MIDDLE); + + /* Run once after a child process has been created. + */ + ap_hook_child_init(h2test_child_init, NULL, NULL, APR_HOOK_MIDDLE); + + /* test h2 handlers */ + ap_hook_handler(h2test_echo_handler, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_handler(h2test_delay_handler, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_handler(h2test_trailer_handler, NULL, NULL, APR_HOOK_MIDDLE); + ap_hook_handler(h2test_error_handler, NULL, NULL, APR_HOOK_MIDDLE); +} + diff --git a/test/modules/http2/mod_h2test/mod_h2test.h b/test/modules/http2/mod_h2test/mod_h2test.h new file mode 100644 index 0000000..a886d29 --- /dev/null +++ b/test/modules/http2/mod_h2test/mod_h2test.h @@ -0,0 +1,21 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef __MOD_H2TEST_H__ +#define __MOD_H2TEST_H__ + + +#endif diff --git a/test/modules/http2/test_001_httpd_alive.py b/test/modules/http2/test_001_httpd_alive.py new file mode 100644 index 0000000..b5708d2 --- /dev/null +++ b/test/modules/http2/test_001_httpd_alive.py @@ -0,0 +1,22 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestBasicAlive: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).add_vhost_test1().install() + assert env.apache_restart() == 0 + + # we expect to see the document from the generic server + def test_h2_001_01(self, env): + url = env.mkurl("https", "test1", "/alive.json") + r = env.curl_get(url, 5) + assert r.exit_code == 0, r.stderr + r.stdout + assert r.response["json"] + assert r.response["json"]["alive"] is True + assert r.response["json"]["host"] == "test1" + diff --git a/test/modules/http2/test_002_curl_basics.py b/test/modules/http2/test_002_curl_basics.py new file mode 100644 index 0000000..91be772 --- /dev/null +++ b/test/modules/http2/test_002_curl_basics.py @@ -0,0 +1,71 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestCurlBasics: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = H2Conf(env) + conf.add_vhost_test1() + conf.add_vhost_test2() + conf.install() + assert env.apache_restart() == 0 + + # check that we see the correct documents when using the test1 server name over http: + def test_h2_002_01(self, env): + url = env.mkurl("http", "test1", "/alive.json") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/1.1" == r.response["protocol"] + assert r.response["json"]["alive"] is True + assert r.response["json"]["host"] == "test1" + + # check that we see the correct documents when using the test1 server name over https: + def test_h2_002_02(self, env): + url = env.mkurl("https", "test1", "/alive.json") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert r.response["json"]["alive"] is True + assert "test1" == r.response["json"]["host"] + assert r.response["header"]["content-type"] == "application/json" + + # enforce HTTP/1.1 + def test_h2_002_03(self, env): + url = env.mkurl("https", "test1", "/alive.json") + r = env.curl_get(url, 5, options=["--http1.1"]) + assert r.response["status"] == 200 + assert r.response["protocol"] == "HTTP/1.1" + + # enforce HTTP/2 + def test_h2_002_04(self, env): + url = env.mkurl("https", "test1", "/alive.json") + r = env.curl_get(url, 5, options=["--http2"]) + assert r.response["status"] == 200 + assert r.response["protocol"] == "HTTP/2" + + # default is HTTP/2 on this host + def test_h2_002_04b(self, env): + url = env.mkurl("https", "test1", "/alive.json") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert r.response["protocol"] == "HTTP/2" + assert r.response["json"]["host"] == "test1" + + # although, without ALPN, we cannot select it + def test_h2_002_05(self, env): + url = env.mkurl("https", "test1", "/alive.json") + r = env.curl_get(url, 5, options=["--no-alpn"]) + assert r.response["status"] == 200 + assert r.response["protocol"] == "HTTP/1.1" + assert r.response["json"]["host"] == "test1" + + # default is HTTP/1.1 on the other + def test_h2_002_06(self, env): + url = env.mkurl("https", "test2", "/alive.json") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert r.response["protocol"] == "HTTP/1.1" + assert r.response["json"]["host"] == "test2" diff --git a/test/modules/http2/test_003_get.py b/test/modules/http2/test_003_get.py new file mode 100644 index 0000000..410097a --- /dev/null +++ b/test/modules/http2/test_003_get.py @@ -0,0 +1,265 @@ +import re +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestGet: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).add_vhost_cgi( + proxy_self=True, h2proxy_self=True + ).add_vhost_test1( + proxy_self=True, h2proxy_self=True + ).install() + assert env.apache_restart() == 0 + + # check SSL environment variables from CGI script + def test_h2_003_01(self, env): + url = env.mkurl("https", "cgi", "/hello.py") + r = env.curl_get(url, 5, options=["--tlsv1.2"]) + assert r.response["status"] == 200 + assert r.response["json"]["protocol"] == "HTTP/2.0" + assert r.response["json"]["https"] == "on" + tls_version = r.response["json"]["ssl_protocol"] + assert tls_version in ["TLSv1.2", "TLSv1.3"] + assert r.response["json"]["h2"] == "on" + assert r.response["json"]["h2push"] == "off" + + r = env.curl_get(url, 5, options=["--http1.1", "--tlsv1.2"]) + assert r.response["status"] == 200 + assert "HTTP/1.1" == r.response["json"]["protocol"] + assert "on" == r.response["json"]["https"] + tls_version = r.response["json"]["ssl_protocol"] + assert tls_version in ["TLSv1.2", "TLSv1.3"] + assert "" == r.response["json"]["h2"] + assert "" == r.response["json"]["h2push"] + + # retrieve a html file from the server and compare it to its source + def test_h2_003_02(self, env): + with open(env.htdocs_src("test1/index.html"), mode='rb') as file: + src = file.read() + + url = env.mkurl("https", "test1", "/index.html") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + assert src == r.response["body"] + + url = env.mkurl("https", "test1", "/index.html") + r = env.curl_get(url, 5, options=["--http1.1"]) + assert r.response["status"] == 200 + assert "HTTP/1.1" == r.response["protocol"] + assert src == r.response["body"] + + # retrieve chunked content from a cgi script + def check_necho(self, env, n, text): + url = env.mkurl("https", "cgi", "/necho.py") + r = env.curl_get(url, 5, options=["-F", f"count={n}", "-F", f"text={text}"]) + assert r.response["status"] == 200 + exp = "" + for i in range(n): + exp += text + "\n" + assert exp == r.response["body"].decode('utf-8') + + def test_h2_003_10(self, env): + self.check_necho(env, 10, "0123456789") + + def test_h2_003_11(self, env): + self.check_necho(env, 100, "0123456789") + + def test_h2_003_12(self, env): + self.check_necho(env, 1000, "0123456789") + + def test_h2_003_13(self, env): + self.check_necho(env, 10000, "0123456789") + + def test_h2_003_14(self, env): + self.check_necho(env, 100000, "0123456789") + + # github issue #126 + def test_h2_003_20(self, env): + url = env.mkurl("https", "test1", "/006/") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + body = r.response["body"].decode('utf-8') + # our doctype varies between branches and in time, lets not compare + body = re.sub(r'^<!DOCTYPE[^>]+>', '', body) + assert ''' +<html> + <head> + <title>Index of /006</title> + </head> + <body> +<h1>Index of /006</h1> +<ul><li><a href="/"> Parent Directory</a></li> +<li><a href="006.css"> 006.css</a></li> +<li><a href="006.js"> 006.js</a></li> +<li><a href="header.html"> header.html</a></li> +</ul> +</body></html> +''' == body + + # github issue #133 + def clean_header(self, s): + s = re.sub(r'\r\n', '\n', s, flags=re.MULTILINE) + s = re.sub(r'^date:.*\n', '', s, flags=re.MULTILINE) + s = re.sub(r'^server:.*\n', '', s, flags=re.MULTILINE) + s = re.sub(r'^last-modified:.*\n', '', s, flags=re.MULTILINE) + s = re.sub(r'^etag:.*\n', '', s, flags=re.MULTILINE) + s = re.sub(r'^vary:.*\n', '', s, flags=re.MULTILINE) + return re.sub(r'^accept-ranges:.*\n', '', s, flags=re.MULTILINE) + + def test_h2_003_21(self, env): + url = env.mkurl("https", "test1", "/index.html") + r = env.curl_get(url, 5, options=["-I"]) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + s = self.clean_header(r.response["body"].decode('utf-8')) + assert '''HTTP/2 200 +content-length: 2007 +content-type: text/html + +''' == s + + r = env.curl_get(url, 5, options=["-I", url]) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + s = self.clean_header(r.response["body"].decode('utf-8')) + assert '''HTTP/2 200 +content-length: 2007 +content-type: text/html + +HTTP/2 200 +content-length: 2007 +content-type: text/html + +''' == s + + # test conditionals: if-modified-since + @pytest.mark.parametrize("path", [ + "/004.html", "/proxy/004.html", "/h2proxy/004.html" + ]) + def test_h2_003_30(self, env, path): + url = env.mkurl("https", "test1", path) + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + h = r.response["header"] + assert "last-modified" in h + lastmod = h["last-modified"] + r = env.curl_get(url, 5, options=['-H', ("if-modified-since: %s" % lastmod)]) + assert 304 == r.response["status"] + + # test conditionals: if-etag + @pytest.mark.parametrize("path", [ + "/004.html", "/proxy/004.html", "/h2proxy/004.html" + ]) + def test_h2_003_31(self, env, path): + url = env.mkurl("https", "test1", path) + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + h = r.response["header"] + assert "etag" in h + etag = h["etag"] + r = env.curl_get(url, 5, options=['-H', ("if-none-match: %s" % etag)]) + assert 304 == r.response["status"] + + # test various response body lengths to work correctly + def test_h2_003_40(self, env): + n = 1001 + while n <= 1025024: + url = env.mkurl("https", "cgi", f"/mnot164.py?count={n}&text=X") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + assert n == len(r.response["body"]) + n *= 2 + + # test various response body lengths to work correctly + @pytest.mark.parametrize("n", [ + 0, 1, 1291, 1292, 80000, 80123, 81087, 98452 + ]) + def test_h2_003_41(self, env, n): + url = env.mkurl("https", "cgi", f"/mnot164.py?count={n}&text=X") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + assert n == len(r.response["body"]) + + # test ranges + @pytest.mark.parametrize("path", [ + "/004.html", "/proxy/004.html", "/h2proxy/004.html" + ]) + def test_h2_003_50(self, env, path): + # check that the resource supports ranges and we see its raw content-length + url = env.mkurl("https", "test1", path) + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"] + h = r.response["header"] + assert "accept-ranges" in h + assert "bytes" == h["accept-ranges"] + assert "content-length" in h + clen = h["content-length"] + # get the first 1024 bytes of the resource, 206 status, but content-length as original + r = env.curl_get(url, 5, options=["-H", "range: bytes=0-1023"]) + assert 206 == r.response["status"] + assert "HTTP/2" == r.response["protocol"] + assert 1024 == len(r.response["body"]) + assert "content-length" in h + assert clen == h["content-length"] + + # use an invalid scheme + def test_h2_003_51(self, env): + url = env.mkurl("https", "cgi", "/") + opt = ["-H:scheme: invalid"] + r = env.nghttp().get(url, options=opt) + assert r.exit_code == 0, r + assert r.response['status'] == 400 + + # use an differing scheme, but one that is acceptable + def test_h2_003_52(self, env): + url = env.mkurl("https", "cgi", "/") + opt = ["-H:scheme: http"] + r = env.nghttp().get(url, options=opt) + assert r.exit_code == 0, r + assert r.response['status'] == 200 + + # Test that we get a proper `Date` and `Server` headers on responses + def test_h2_003_60(self, env): + url = env.mkurl("https", "test1", "/index.html") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 200 + assert 'date' in r.response['header'] + assert 'server' in r.response['header'] + + # lets do some error tests + def test_h2_003_70(self, env): + url = env.mkurl("https", "cgi", "/h2test/error?status=500") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 500 + url = env.mkurl("https", "cgi", "/h2test/error?error=timeout") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 408 + + # produce an error during response body + def test_h2_003_71(self, env, repeat): + url = env.mkurl("https", "cgi", "/h2test/error?body_error=timeout") + r = env.curl_get(url) + assert r.exit_code != 0, f"{r}" + url = env.mkurl("https", "cgi", "/h2test/error?body_error=reset") + r = env.curl_get(url) + assert r.exit_code != 0, f"{r}" + + # produce an error, fail to generate an error bucket + def test_h2_003_72(self, env, repeat): + url = env.mkurl("https", "cgi", "/h2test/error?body_error=timeout&error_bucket=0") + r = env.curl_get(url) + assert r.exit_code != 0, f"{r}" diff --git a/test/modules/http2/test_004_post.py b/test/modules/http2/test_004_post.py new file mode 100644 index 0000000..44f31d2 --- /dev/null +++ b/test/modules/http2/test_004_post.py @@ -0,0 +1,219 @@ +import difflib +import email.parser +import inspect +import json +import os +import re +import sys +import time + +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestPost: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + TestPost._local_dir = os.path.dirname(inspect.getfile(TestPost)) + H2Conf(env).add_vhost_cgi().install() + assert env.apache_restart() == 0 + + def local_src(self, fname): + return os.path.join(TestPost._local_dir, fname) + + # 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, f"{r}" + assert 200 <= r.response["status"] < 300 + + r2 = env.curl_get(r.response["header"]["location"]) + assert r2.exit_code == 0 + assert r2.response["status"] == 200 + with open(self.local_src(fpath), mode='rb') as file: + src = file.read() + assert src == r2.response["body"] + + def test_h2_004_01(self, env): + self.curl_upload_and_verify(env, "data-1k", ["-vvv", "--http1.1"]) + self.curl_upload_and_verify(env, "data-1k", ["--http2"]) + + def test_h2_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_h2_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_h2_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_h2_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_h2_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+-\d+-1'), + ]) + def test_h2_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)) + + # 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(self.local_src(fpath), mode='rb') as file: + src = file.read() + assert 'request-length' in r.response["header"] + assert int(r.response["header"]['request-length']) == len(src) + if len(r.response["body"]) != len(src): + sys.stderr.writelines(difflib.unified_diff( + src.decode().splitlines(True), + r.response["body"].decode().splitlines(True), + fromfile='source', + tofile='response' + )) + assert len(r.response["body"]) == len(src) + assert r.response["body"] == src, f"expected '{src}', got '{r.response['body']}'" + + @pytest.mark.parametrize("name", [ + "data-1k", "data-10k", "data-100k", "data-1m" + ]) + def test_h2_004_21(self, env, name): + self.nghttp_post_and_verify(env, name, []) + + @pytest.mark.parametrize("name", [ + "data-1k", "data-10k", "data-100k", "data-1m", + ]) + def test_h2_004_22(self, env, name, repeat): + self.nghttp_post_and_verify(env, name, ["--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(self.local_src(fpath), mode='rb') as file: + src = file.read() + assert src == r2.response["body"] + + @pytest.mark.parametrize("name", [ + "data-1k", "data-10k", "data-100k", "data-1m" + ]) + def test_h2_004_23(self, env, name, repeat): + self.nghttp_upload_and_verify(env, name, []) + + @pytest.mark.parametrize("name", [ + "data-1k", "data-10k", "data-100k", "data-1m" + ]) + def test_h2_004_24(self, env, name, repeat): + self.nghttp_upload_and_verify(env, name, ["--expect-continue"]) + + @pytest.mark.parametrize("name", [ + "data-1k", "data-10k", "data-100k", "data-1m" + ]) + def test_h2_004_25(self, env, name, repeat): + self.nghttp_upload_and_verify(env, name, ["--no-content-length"]) + + def test_h2_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) + H2Conf(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, options=["--http2"]) + assert r.response["status"] == 200 + r = env.curl_get(url, 5, options=["--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, options=["--http2", "-H", "Range: bytes=0-{0}".format(chunk-1)]) + assert 206 == r.response["status"] + assert chunk == len(r.response["body"].decode('utf-8')) + # Wait for log completeness + time.sleep(1) + # 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 received + 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_h2_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(self.local_src(fpath), mode='rb') as file: + src = file.read() + assert src == filepart.get_payload(decode=True) + + post_and_verify("data-1k", []) diff --git a/test/modules/http2/test_005_files.py b/test/modules/http2/test_005_files.py new file mode 100644 index 0000000..e761836 --- /dev/null +++ b/test/modules/http2/test_005_files.py @@ -0,0 +1,48 @@ +import os +import pytest + +from .env import H2Conf, H2TestEnv + + +def mk_text_file(fpath: str, lines: int): + t110 = "" + for _ in range(11): + t110 += "0123456789" + with open(fpath, "w") as fd: + for i in range(lines): + fd.write("{0:015d}: ".format(i)) # total 128 bytes per line + fd.write(t110) + fd.write("\n") + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestFiles: + + URI_PATHS = [] + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + docs_a = os.path.join(env.server_docs_dir, "cgi/files") + uris = [] + file_count = 32 + file_sizes = [1, 10, 100, 10000] + for i in range(file_count): + fsize = file_sizes[i % len(file_sizes)] + if fsize is None: + raise Exception("file sizes?: {0} {1}".format(i, fsize)) + fname = "{0}-{1}k.txt".format(i, fsize) + mk_text_file(os.path.join(docs_a, fname), 8 * fsize) + self.URI_PATHS.append(f"/files/{fname}") + + H2Conf(env).add_vhost_cgi( + proxy_self=True, h2proxy_self=True + ).add_vhost_test1( + proxy_self=True, h2proxy_self=True + ).install() + assert env.apache_restart() == 0 + + def test_h2_005_01(self, env): + url = env.mkurl("https", "cgi", self.URI_PATHS[2]) + r = env.curl_get(url) + assert r.response, r.stderr + r.stdout + assert r.response["status"] == 200 diff --git a/test/modules/http2/test_006_assets.py b/test/modules/http2/test_006_assets.py new file mode 100644 index 0000000..778314e --- /dev/null +++ b/test/modules/http2/test_006_assets.py @@ -0,0 +1,75 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestAssets: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).add_vhost_test1().install() + assert env.apache_restart() == 0 + + # single page without any assets + def test_h2_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_h2_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_h2_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_h2_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_h2_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"} + ] diff --git a/test/modules/http2/test_100_conn_reuse.py b/test/modules/http2/test_100_conn_reuse.py new file mode 100644 index 0000000..3ebac24 --- /dev/null +++ b/test/modules/http2/test_100_conn_reuse.py @@ -0,0 +1,57 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestConnReuse: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(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_h2_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_h2_100_02(self, env): + url = env.mkurl("https", "cgi", "/hello.py") + hostname = ("cgi-alias.%s" % env.http_tld) + r = env.curl_get(url, 5, options=["-H", f"Host: {hostname}"]) + assert r.response["status"] == 200 + 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_h2_100_03(self, env): + url = env.mkurl("https", "cgi", "/") + hostname = ("test1.%s" % env.http_tld) + r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) + assert r.response["status"] == 200 + 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_h2_100_04(self, env): + url = env.mkurl("https", "cgi", "/hello.py") + hostname = ("noh2.%s" % env.http_tld) + r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) + assert 421 == r.response["status"] + + # access an unknown vhost, after using ServerName in SNI + def test_h2_100_05(self, env): + url = env.mkurl("https", "cgi", "/hello.py") + hostname = ("unknown.%s" % env.http_tld) + r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ]) + assert 421 == r.response["status"] diff --git a/test/modules/http2/test_101_ssl_reneg.py b/test/modules/http2/test_101_ssl_reneg.py new file mode 100644 index 0000000..66f2638 --- /dev/null +++ b/test/modules/http2/test_101_ssl_reneg.py @@ -0,0 +1,130 @@ +import re +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +@pytest.mark.skipif(H2TestEnv.get_ssl_module() != "mod_ssl", reason="only for mod_ssl") +class TestSslRenegotiation: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + domain = f"ssl.{env.http_tld}" + conf = H2Conf(env, extras={ + 'base': [ + "SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384", + f"<Directory \"{env.server_dir}/htdocs/ssl-client-verify\">", + " Require all granted", + " SSLVerifyClient require", + " SSLVerifyDepth 0", + "</Directory>" + ], + domain: [ + "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>", + f"<Directory \"{env.server_dir}/htdocs/sslrequire\">", + " SSLRequireSSL", + "</Directory>", + f"<Directory \"{env.server_dir}/htdocs/requiressl\">", + " Require ssl", + "</Directory>", + ]}) + conf.add_vhost(domains=[domain], port=env.https_port, + doc_root=f"{env.server_dir}/htdocs") + conf.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_h2_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, f"{r}" + assert r.response + assert 403 == r.response["status"] + + # try to renegotiate the cipher, should fail with correct code + def test_h2_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_h2_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_h2_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, f"{r}" + 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_h2_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_h2_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_h2_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_h2_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) diff --git a/test/modules/http2/test_102_require.py b/test/modules/http2/test_102_require.py new file mode 100644 index 0000000..b7e4eae --- /dev/null +++ b/test/modules/http2/test_102_require.py @@ -0,0 +1,41 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestRequire: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + domain = f"ssl.{env.http_tld}" + conf = H2Conf(env) + conf.start_vhost(domains=[domain], port=env.https_port) + 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(f"{env.server_dir}/htdocs/ssl-client-verify") + assert env.apache_restart() == 0 + + def test_h2_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_h2_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"] diff --git a/test/modules/http2/test_103_upgrade.py b/test/modules/http2/test_103_upgrade.py new file mode 100644 index 0000000..2fa7d1d --- /dev/null +++ b/test/modules/http2/test_103_upgrade.py @@ -0,0 +1,118 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestUpgrade: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).add_vhost_test1().add_vhost_test2().add_vhost_noh2( + ).start_vhost(domains=[f"test3.{env.http_tld}"], port=env.https_port, doc_root="htdocs/test1" + ).add( + """ + Protocols h2 http/1.1 + Header unset Upgrade""" + ).end_vhost( + ).start_vhost(domains=[f"test1b.{env.http_tld}"], port=env.http_port, doc_root="htdocs/test1" + ).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_h2_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_h2_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_h2_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_h2_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_h2_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_h2_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_h2_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_h2_103_20(self, env): + url = env.mkurl("http", "test1", "/index.html") + r = env.nghttp().get(url, options=["-u"]) + assert r.response["status"] == 200 + + # upgrade to h2c for a request where http/1.1 is preferred, but the clients upgrade + # wish is honored nevertheless + def test_h2_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_h2_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_h2_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_h2_103_24(self, env): + url = env.mkurl("http", "test1b", "/006.html") + r = env.nghttp().get(url, options=["-u"]) + assert r.response["status"] == 200 diff --git a/test/modules/http2/test_104_padding.py b/test/modules/http2/test_104_padding.py new file mode 100644 index 0000000..7b874ed --- /dev/null +++ b/test/modules/http2/test_104_padding.py @@ -0,0 +1,98 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +def frame_padding(payload, padbits): + mask = (1 << padbits) - 1 + return ((payload + 9 + mask) & ~mask) - (payload + 9) + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestPadding: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = H2Conf(env) + conf.start_vhost(domains=[f"ssl.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") + conf.add("AddHandler cgi-script .py") + conf.end_vhost() + conf.start_vhost(domains=[f"pad0.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") + conf.add("H2Padding 0") + conf.add("AddHandler cgi-script .py") + conf.end_vhost() + conf.start_vhost(domains=[f"pad1.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") + conf.add("H2Padding 1") + conf.add("AddHandler cgi-script .py") + conf.end_vhost() + conf.start_vhost(domains=[f"pad2.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") + conf.add("H2Padding 2") + conf.add("AddHandler cgi-script .py") + conf.end_vhost() + conf.start_vhost(domains=[f"pad3.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") + conf.add("H2Padding 3") + conf.add("AddHandler cgi-script .py") + conf.end_vhost() + conf.start_vhost(domains=[f"pad8.{env.http_tld}"], port=env.https_port, doc_root="htdocs/cgi") + 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_h2_104_01(self, env): + url = env.mkurl("https", "ssl", "/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 r.response["status"] == 200 + assert r.results["paddings"] == [ + frame_padding(len(data)+1, 0), + frame_padding(0, 0) + ] + + # 0 bits of padding + def test_h2_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 r.response["status"] == 200 + assert r.results["paddings"] == [0, 0] + + # 1 bit of padding + def test_h2_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 r.response["status"] == 200 + for i in r.results["paddings"]: + assert i in range(0, 2) + + # 2 bits of padding + def test_h2_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 r.response["status"] == 200 + for i in r.results["paddings"]: + assert i in range(0, 4) + + # 3 bits of padding + def test_h2_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 r.response["status"] == 200 + for i in r.results["paddings"]: + assert i in range(0, 8) + + # 8 bits of padding + def test_h2_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 r.response["status"] == 200 + for i in r.results["paddings"]: + assert i in range(0, 256) diff --git a/test/modules/http2/test_105_timeout.py b/test/modules/http2/test_105_timeout.py new file mode 100644 index 0000000..13aa8ed --- /dev/null +++ b/test/modules/http2/test_105_timeout.py @@ -0,0 +1,149 @@ +import socket +import time + +import pytest + +from .env import H2Conf +from pyhttpd.curl import CurlPiper + + +class TestTimeout: + + # Check that base servers 'Timeout' setting is observed on SSL handshake + def test_h2_105_01(self, env): + conf = H2Conf(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_h2_105_02(self, env): + conf = H2Conf(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_h2_105_03(self, env): + conf = H2Conf(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, options=[ + "-vvv", + "-F", ("count=%d" % 100), + "-F", ("text=%s" % "abcdefghijklmnopqrstuvwxyz"), + "-F", ("wait1=%f" % 1.5), + ]) + assert r.response["status"] == 200 + + def test_h2_105_10(self, env): + # just a check without delays if all is fine + conf = H2Conf(env) + conf.add_vhost_cgi() + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2test/delay") + piper = CurlPiper(env=env, url=url) + piper.start() + stdout, stderr = piper.close() + assert piper.exitcode == 0 + assert len("".join(stdout)) == 3 * 8192 + + def test_h2_105_11(self, env): + # short connection timeout, longer stream delay + # connection timeout must not abort ongoing streams + conf = H2Conf(env) + conf.add_vhost_cgi() + conf.add("Timeout 1") + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2test/delay?1200ms") + piper = CurlPiper(env=env, url=url) + piper.start() + stdout, stderr = piper.close() + assert len("".join(stdout)) == 3 * 8192 + + def test_h2_105_12(self, env): + # long connection timeout, short stream timeout + # sending a slow POST + if env.httpd_is_at_least("2.5.0"): + conf = H2Conf(env) + conf.add_vhost_cgi() + conf.add("Timeout 10") + conf.add("H2StreamTimeout 1") + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2test/delay?5") + piper = CurlPiper(env=env, url=url) + piper.start() + for _ in range(3): + time.sleep(2) + try: + piper.send("0123456789\n") + except BrokenPipeError: + break + piper.close() + assert piper.response + assert piper.response['status'] == 408, f"{piper.response}" diff --git a/test/modules/http2/test_106_shutdown.py b/test/modules/http2/test_106_shutdown.py new file mode 100644 index 0000000..b119292 --- /dev/null +++ b/test/modules/http2/test_106_shutdown.py @@ -0,0 +1,69 @@ +# +# mod-h2 test suite +# check HTTP/2 timeout behaviour +# +import time +from threading import Thread + +import pytest + +from .env import H2Conf, H2TestEnv +from pyhttpd.result import ExecResult + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestShutdown: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = H2Conf(env) + conf.add_vhost_cgi() + conf.install() + assert env.apache_restart() == 0 + + def test_h2_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, options=args) + + t = Thread(target=long_request) + t.start() + time.sleep(0.5) + assert env.apache_reload() == 0 + t.join() + # noinspection PyTypeChecker + time.sleep(1) + r: ExecResult = self.r + assert r.exit_code == 0 + assert r.response, f"no response via {r.args} in {r.stderr}\nstdout: {len(r.stdout)} bytes" + assert r.response["status"] == 200, f"{r}" + assert len(r.response["body"]) == (lines * (len(text)+1)), f"{r}" + + def test_h2_106_02(self, env): + # PR65731: invalid GOAWAY frame at session start when + # MaxRequestsPerChild is reached + # Create a low limit and only 2 children, so we'll encounter this easily + conf = H2Conf(env, extras={ + 'base': [ + "ServerLimit 2", + "MaxRequestsPerChild 3" + ] + }) + conf.add_vhost_test1() + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "test1", "/index.html") + for i in range(7): + r = env.curl_get(url, options=['-vvv']) + assert r.exit_code == 0, f"failed on {i}. request: {r.stdout} {r.stderr}" + assert r.response["status"] == 200 + assert "HTTP/2" == r.response["protocol"]
\ No newline at end of file diff --git a/test/modules/http2/test_200_header_invalid.py b/test/modules/http2/test_200_header_invalid.py new file mode 100644 index 0000000..fdbfbe4 --- /dev/null +++ b/test/modules/http2/test_200_header_invalid.py @@ -0,0 +1,182 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +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 gets aborted with a h2 error and + # there will be no http status, cr and lf are handled special + def test_h2_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_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 + 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_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) + 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_h2_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 r.response["status"] == 200 + r = env.curl_get(url, options=["-H", "x: %sx" % (val[:8188])]) + assert 431 == r.response["status"] + # same with field name + r = env.curl_get(url, options=["-H", "y%s: 1" % (val[:8186])]) + assert r.response["status"] == 200 + r = env.curl_get(url, options=["-H", "y%s: 1" % (val[:8188])]) + assert 431 == r.response["status"] + + # test header field lengths check, LimitRequestFieldSize (default 8190) + def test_h2_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 r.response["status"] == 200 + 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_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"] == 200 + + # 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", "/") + 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 = 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): + 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 + # nghttp version >= 1.45.0 check pseudo headers and RST streams, + # which means we see no response. + if r.response is not None: + 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 + if r.response is not None: + assert r.response["status"] == 400 diff --git a/test/modules/http2/test_201_header_conditional.py b/test/modules/http2/test_201_header_conditional.py new file mode 100644 index 0000000..f103268 --- /dev/null +++ b/test/modules/http2/test_201_header_conditional.py @@ -0,0 +1,70 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestConditionalHeaders: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(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_h2_201_01(self, env): + url = env.mkurl("https", "test1", "/006/006.css") + r = env.curl_get(url) + assert r.response["status"] == 200 + 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 r.response["status"] == 200 + + # check handling of 'if-none-match' header + def test_h2_201_02(self, env): + url = env.mkurl("https", "test1", "/006/006.css") + r = env.curl_get(url) + assert r.response["status"] == 200 + 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 r.response["status"] == 200 + + @pytest.mark.skipif(True, reason="304 misses the Vary header in trunk and 2.4.x") + def test_h2_201_03(self, env): + url = env.mkurl("https", "test1", "/006.html") + r = env.curl_get(url, options=["-H", "Accept-Encoding: gzip"]) + assert r.response["status"] == 200 + 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_h2_201_04(self, env): + url = env.mkurl("https", "test1", "/006.html") + r = env.curl_get(url, options=["--http1.1", "-H", "Connection: keep-alive"]) + assert r.response["status"] == 200 + assert "timeout=30, max=30" == r.response["header"]["keep-alive"] + r = env.curl_get(url, options=["-H", "Connection: keep-alive"]) + assert r.response["status"] == 200 + assert "keep-alive" not in r.response["header"] diff --git a/test/modules/http2/test_202_trailer.py b/test/modules/http2/test_202_trailer.py new file mode 100644 index 0000000..4b4fc42 --- /dev/null +++ b/test/modules/http2/test_202_trailer.py @@ -0,0 +1,92 @@ +import os +import pytest + +from .env import H2Conf + + +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 TestTrailers: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + setup_data(env) + conf = H2Conf(env, extras={ + f"cgi.{env.http_tld}": [ + "<Location \"/h2test/trailer\">", + " SetHandler h2test-trailer", + "</Location>" + ], + }) + conf.add_vhost_cgi(h2proxy_self=True) + conf.install() + assert env.apache_restart() == 0 + + # check if the server survives a trailer or two + def test_h2_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 r.response["status"] < 300 + assert len(r.response["body"]) == 1000 + + r = env.nghttp().upload(url, fpath, options=["--trailer", "test: 1b", "--trailer", "XXX: test"]) + assert r.response["status"] < 300 + assert len(r.response["body"]) == 1000 + + # check if the server survives a trailer without content-length + def test_h2_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 r.response["status"] < 300 + assert len(r.response["body"]) == 1000 + + # check if echoing request headers in response from GET works + def test_h2_202_03(self, env): + url = env.mkurl("https", "cgi", "/echohd.py?name=X") + r = env.nghttp().get(url, options=["--header", "X: 3"]) + assert r.response["status"] < 300 + assert r.response["body"] == b"X: 3\n" + + # check if echoing request headers in response from POST works + def test_h2_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 r.response["status"] < 300 + assert r.response["body"] == b"X: 3b\n" + + # check if echoing request headers in response from POST works, but trailers are not seen + # This is the way CGI invocation works. + def test_h2_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 r.response["status"] < 300 + assert r.response["body"] == b"X: 4a\n" + + # check that our h2test-trailer handler works + def test_h2_202_10(self, env): + url = env.mkurl("https", "cgi", "/h2test/trailer?1024") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + assert len(r.response["body"]) == 1024 + assert 'trailer' in r.response + assert 'trailer-content-length' in r.response['trailer'] + assert r.response['trailer']['trailer-content-length'] == '1024' + + # check that trailers also for with empty bodies + def test_h2_202_11(self, env): + url = env.mkurl("https", "cgi", "/h2test/trailer?0") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + assert len(r.response["body"]) == 0, f'{r.response["body"]}' + assert 'trailer' in r.response + assert 'trailer-content-length' in r.response['trailer'] + assert r.response['trailer']['trailer-content-length'] == '0' diff --git a/test/modules/http2/test_203_rfc9113.py b/test/modules/http2/test_203_rfc9113.py new file mode 100644 index 0000000..9fc8f3b --- /dev/null +++ b/test/modules/http2/test_203_rfc9113.py @@ -0,0 +1,56 @@ +import pytest + +from pyhttpd.env import HttpdTestEnv +from .env import H2Conf + + +class TestRfc9113: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).add_vhost_test1().install() + assert env.apache_restart() == 0 + + # by default, we accept leading/trailing ws in request fields + def test_h2_203_01_ws_ignore(self, env): + url = env.mkurl("https", "test1", "/") + r = env.curl_get(url, options=['-H', 'trailing-space: must not ']) + assert r.exit_code == 0, f'curl output: {r.stderr}' + assert r.response["status"] == 200, f'curl output: {r.stdout}' + r = env.curl_get(url, options=['-H', 'trailing-space: must not\t']) + assert r.exit_code == 0, f'curl output: {r.stderr}' + assert r.response["status"] == 200, f'curl output: {r.stdout}' + + # response header are also handled, but we strip ws before sending + @pytest.mark.parametrize(["hvalue", "expvalue", "status"], [ + ['"123"', '123', 200], + ['"123 "', '123', 200], # trailing space stripped + ['"123\t"', '123', 200], # trailing tab stripped + ['" 123"', '123', 200], # leading space is stripped + ['" 123"', '123', 200], # leading spaces are stripped + ['"\t123"', '123', 200], # leading tab is stripped + ['"expr=%{unescape:123%0A 123}"', '', 500], # illegal char + ['" \t "', '', 200], # just ws + ]) + def test_h2_203_02(self, env, hvalue, expvalue, status): + hname = 'ap-test-007' + conf = H2Conf(env, extras={ + f'test1.{env.http_tld}': [ + '<Location /index.html>', + f'Header add {hname} {hvalue}', + '</Location>', + ] + }) + conf.add_vhost_test1(proxy_self=True) + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "test1", "/index.html") + r = env.curl_get(url, options=['--http2']) + if status == 500 and r.exit_code != 0: + # in 2.4.x we fail late on control chars in a response + # and RST_STREAM. That's also ok + return + assert r.response["status"] == status + if int(status) < 400: + assert r.response["header"][hname] == expvalue + diff --git a/test/modules/http2/test_300_interim.py b/test/modules/http2/test_300_interim.py new file mode 100644 index 0000000..774ab88 --- /dev/null +++ b/test/modules/http2/test_300_interim.py @@ -0,0 +1,40 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestInterimResponses: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(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_h2_300_01(self, env): + url = env.mkurl("https", "test1", "/index.html") + r = env.curl_post_data(url, 'XYZ') + assert r.response["status"] == 200 + assert "previous" not in r.response + + # check that we see an interim response when we ask for it + def test_h2_300_02(self, env): + url = env.mkurl("https", "cgi", "/echo.py") + r = env.curl_post_data(url, 'XYZ', options=["-H", "expect: 100-continue"]) + assert r.response["status"] == 200 + assert "previous" in r.response + assert 100 == r.response["previous"]["status"] + + # check proper answer on unexpected + def test_h2_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 diff --git a/test/modules/http2/test_400_push.py b/test/modules/http2/test_400_push.py new file mode 100644 index 0000000..9c61608 --- /dev/null +++ b/test/modules/http2/test_400_push.py @@ -0,0 +1,200 @@ +import os +import pytest + +from .env import H2Conf, H2TestEnv + + +# The push tests depend on "nghttp" +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestPush: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).start_vhost(domains=[f"push.{env.http_tld}"], + port=env.https_port, doc_root="htdocs/test1" + ).add(r""" + 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_h2_400_00(self, env): + url = env.mkurl("https", "push", "/006.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 0 == len(promises) + + # 2 link headers configured, only 1 triggers push + def test_h2_400_01(self, env): + url = env.mkurl("https", "push", "/006-push.html") + r = env.nghttp().get(url, options=["-Haccept-encoding: none"]) + assert r.response["status"] == 200 + 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_h2_400_02(self, env): + url = env.mkurl("https", "push", "/006-push2.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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 attribute + def test_h2_400_03(self, env): + url = env.mkurl("https", "push", "/006-push3.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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_h2_400_04(self, env): + url = env.mkurl("https", "push", "/006-push4.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 0 == len(promises) + + # More than one value in "rel" parameter + def test_h2_400_05(self, env): + url = env.mkurl("https", "push", "/006-push5.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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_h2_400_06(self, env): + url = env.mkurl("https", "push", "/006-push6.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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_h2_400_07(self, env): + url = env.mkurl("https", "push", "/006-push7.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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_h2_400_08(self, env): + url = env.mkurl("https", "push", "/006-push8.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 0 == len(promises) + + # 2 H2PushResource config trigger on GET, but not on POST + def test_h2_400_20(self, env): + url = env.mkurl("https", "push", "/006-push20.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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 r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 0 == len(promises) + + # H2Push configured Off in location + def test_h2_400_30(self, env): + url = env.mkurl("https", "push", "/006-push30.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 0 == len(promises) + + # - suppress PUSH + def test_h2_400_50(self, env): + url = env.mkurl("https", "push", "/006-push.html") + r = env.nghttp().get(url, options=['-H', 'accept-push-policy: none']) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 0 == len(promises) + + # - default pushes desired + def test_h2_400_51(self, env): + url = env.mkurl("https", "push", "/006-push.html") + r = env.nghttp().get(url, options=['-H', 'accept-push-policy: default']) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 1 == len(promises) + + # - HEAD pushes desired + def test_h2_400_52(self, env): + url = env.mkurl("https", "push", "/006-push.html") + r = env.nghttp().get(url, options=['-H', 'accept-push-policy: head']) + assert r.response["status"] == 200 + 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_h2_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 r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 1 == len(promises) diff --git a/test/modules/http2/test_401_early_hints.py b/test/modules/http2/test_401_early_hints.py new file mode 100644 index 0000000..f73dcc4 --- /dev/null +++ b/test/modules/http2/test_401_early_hints.py @@ -0,0 +1,47 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +# The push tests depend on "nghttp" +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestEarlyHints: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).start_vhost(domains=[f"hints.{env.http_tld}"], + port=env.https_port, doc_root="htdocs/test1" + ).add(""" + 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_h2_401_31(self, env, repeat): + url = env.mkurl("https", "hints", "/006-hints.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + 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_h2_401_32(self, env, repeat): + url = env.mkurl("https", "hints", "/006-nohints.html") + r = env.nghttp().get(url) + assert r.response["status"] == 200 + promises = r.results["streams"][r.response["id"]]["promises"] + assert 1 == len(promises) + assert "previous" not in r.response diff --git a/test/modules/http2/test_500_proxy.py b/test/modules/http2/test_500_proxy.py new file mode 100644 index 0000000..2e61415 --- /dev/null +++ b/test/modules/http2/test_500_proxy.py @@ -0,0 +1,151 @@ +import inspect +import os +import re +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestProxy: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(env).add_vhost_cgi(proxy_self=True).install() + assert env.apache_restart() == 0 + + def local_src(self, fname): + return os.path.join(os.path.dirname(inspect.getfile(TestProxy)), fname) + + def setup_method(self, method): + print("setup_method: %s" % method.__name__) + + def teardown_method(self, method): + print("teardown_method: %s" % method.__name__) + + def test_h2_500_01(self, env): + url = env.mkurl("https", "cgi", "/proxy/hello.py") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + 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(self.local_src(fpath), mode='rb') as file: + src = file.read() + assert r2.response["body"] == src + + def test_h2_500_10(self, env, repeat): + 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(self.local_src(fpath), mode='rb') as file: + src = file.read() + if r.response["body"] != src: + with open(os.path.join(env.gen_dir, "nghttp.out"), 'w') as fd: + fd.write(r.outraw.decode()) + fd.write("\nstderr:\n") + fd.write(r.stderr) + assert r.response["body"] == src + + def test_h2_500_20(self, env, repeat): + 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_h2_500_21(self, env, repeat): + 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(self.local_src(fpath), mode='rb') as file: + src = file.read() + assert src == r2.response["body"] + + def test_h2_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_h2_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_h2_500_24(self, env): + for i in range(100): + self.nghttp_upload_stat(env, "data-1k", ["--no-content-length"]) + + # lets do some error tests + def test_h2_500_30(self, env): + url = env.mkurl("https", "cgi", "/proxy/h2test/error?status=500") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 500 + url = env.mkurl("https", "cgi", "/proxy/h2test/error?error=timeout") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 408 + + # produce an error during response body + def test_h2_500_31(self, env, repeat): + url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout") + r = env.curl_get(url) + assert r.exit_code != 0, r + + # produce an error, fail to generate an error bucket + def test_h2_500_32(self, env, repeat): + url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout&error_bucket=0") + r = env.curl_get(url) + assert r.exit_code != 0, r diff --git a/test/modules/http2/test_501_proxy_serverheader.py b/test/modules/http2/test_501_proxy_serverheader.py new file mode 100644 index 0000000..0d7c188 --- /dev/null +++ b/test/modules/http2/test_501_proxy_serverheader.py @@ -0,0 +1,36 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestProxyServerHeader: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + "Header unset Server", + "Header always set Server cgi", + ] + }) + conf.add_vhost_cgi(proxy_self=True, h2proxy_self=False) + conf.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_h2_501_01(self, env): + url = env.mkurl("https", "cgi", "/proxy/hello.py") + r = env.curl_get(url, 5) + assert r.response["status"] == 200 + 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"] + assert "cgi" == r.response["header"]["server"] diff --git a/test/modules/http2/test_502_proxy_port.py b/test/modules/http2/test_502_proxy_port.py new file mode 100644 index 0000000..f6c6db1 --- /dev/null +++ b/test/modules/http2/test_502_proxy_port.py @@ -0,0 +1,41 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestProxyPort: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = H2Conf(env, extras={ + 'base': [ + f'Listen {env.proxy_port}', + 'Protocols h2c http/1.1', + 'LogLevel proxy_http2:trace2 proxy:trace2', + ], + f'cgi.{env.http_tld}': [ + "Header unset Server", + "Header always set Server cgi", + ] + }) + conf.add_vhost_cgi(proxy_self=False, h2proxy_self=False) + conf.start_vhost(domains=[f"test1.{env.http_tld}"], port=env.proxy_port) + conf.add([ + 'Protocols h2c', + 'RewriteEngine On', + 'RewriteRule "^/(.*)$" "h2c://%{HTTP_HOST}/$1"[NC,P]', + 'ProxyPassMatch / "h2c://$1/"', + ]) + conf.end_vhost() + conf.install() + assert env.apache_restart() == 0 + + # Test PR 65881 + # h2c upgraded request via a dynamic proxy onto another port + def test_h2_502_01(self, env): + url = f'http://localhost:{env.http_port}/hello.py' + r = env.curl_get(url, 5, options=['--http2', + '--proxy', f'localhost:{env.proxy_port}']) + assert r.response['status'] == 200 + assert r.json['port'] == f'{env.http_port}' diff --git a/test/modules/http2/test_600_h2proxy.py b/test/modules/http2/test_600_h2proxy.py new file mode 100644 index 0000000..e93ba1a --- /dev/null +++ b/test/modules/http2/test_600_h2proxy.py @@ -0,0 +1,175 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestH2Proxy: + + def test_h2_600_01(self, env): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + "SetEnvIf Host (.+) X_HOST=$1", + ] + }) + conf.add_vhost_cgi(h2proxy_self=True) + conf.install() + assert env.apache_restart() == 0 + 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}:{env.https_port}" + + def test_h2_600_02(self, env): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + "SetEnvIf Host (.+) X_HOST=$1", + f"ProxyPreserveHost on", + f"ProxyPass /h2c/ h2c://127.0.0.1:{env.http_port}/", + ] + }) + conf.add_vhost_cgi() + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2c/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"] == "" + # the proxied backend sees Host header as passed on front + assert r.response["json"]["host"] == f"cgi.{env.http_tld}:{env.https_port}" + assert r.response["json"]["h2_original_host"] == "" + + def test_h2_600_03(self, env): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + "SetEnvIf Host (.+) X_HOST=$1", + f"ProxyPreserveHost off", + f"ProxyPass /h2c/ h2c://127.0.0.1:{env.http_port}/", + ] + }) + conf.add_vhost_cgi() + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2c/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"] == "" + # the proxied backend sees Host as using in connecting to it + assert r.response["json"]["host"] == f"127.0.0.1:{env.http_port}" + assert r.response["json"]["h2_original_host"] == "" + + # check that connection reuse actually happens as configured + @pytest.mark.parametrize("enable_reuse", [ "on", "off" ]) + def test_h2_600_04(self, env, enable_reuse): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + f"ProxyPassMatch ^/h2proxy/([0-9]+)/(.*)$ " + f" h2c://127.0.0.1:$1/$2 enablereuse={enable_reuse} keepalive=on", + ] + }) + conf.add_vhost_cgi() + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py") + if enable_reuse == "on": + # reuse is not guaranteed for each request, but we expect some + # to do it and run on a h2 stream id > 1 + reused = False + count = 10 + r = env.curl_raw([url] * count, 5) + response = r.response + for n in range(count): + assert response["status"] == 200 + if n == (count - 1): + break + response = response["previous"] + assert r.json[0]["h2_stream_id"] == "1" + for n in range(1, count): + if int(r.json[n]["h2_stream_id"]) > 1: + reused = True + break + assert reused + else: + r = env.curl_raw([url, url], 5) + assert r.response["previous"]["status"] == 200 + assert r.response["status"] == 200 + assert r.json[0]["h2_stream_id"] == "1" + assert r.json[1]["h2_stream_id"] == "1" + + # do some flexible setup from #235 to proper connection selection + @pytest.mark.parametrize("enable_reuse", [ "on", "off" ]) + def test_h2_600_05(self, env, enable_reuse): + conf = H2Conf(env, extras={ + f'cgi.{env.http_tld}': [ + f"ProxyPassMatch ^/h2proxy/([0-9]+)/(.*)$ " + f" h2c://127.0.0.1:$1/$2 enablereuse={enable_reuse} keepalive=on", + ] + }) + conf.add_vhost_cgi() + conf.add([ + f'Listen {env.http_port2}', + 'UseCanonicalName On', + 'UseCanonicalPhysicalPort On' + ]) + conf.start_vhost(domains=[f'cgi.{env.http_tld}'], + port=5004, doc_root="htdocs/cgi") + conf.add("AddHandler cgi-script .py") + conf.end_vhost() + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py") + url2 = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port2}/hello.py") + r = env.curl_raw([url, url2], 5) + assert r.response["previous"]["status"] == 200 + assert int(r.json[0]["port"]) == env.http_port + assert r.response["status"] == 200 + exp_port = env.http_port if enable_reuse == "on" else env.http_port2 + assert int(r.json[1]["port"]) == exp_port + + # lets do some error tests + def test_h2_600_30(self, env): + conf = H2Conf(env) + conf.add_vhost_cgi(h2proxy_self=True) + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?status=500") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 500 + url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?error=timeout") + r = env.curl_get(url) + assert r.exit_code == 0, r + assert r.response['status'] == 408 + + # produce an error during response body + def test_h2_600_31(self, env, repeat): + conf = H2Conf(env) + conf.add_vhost_cgi(h2proxy_self=True) + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?body_error=timeout") + r = env.curl_get(url) + # depending on when the error is detect in proxying, if may RST the + # stream (exit_code != 0) or give a 503 response. + if r.exit_code == 0: + assert r.response['status'] == 503 + + # produce an error, fail to generate an error bucket + def test_h2_600_32(self, env, repeat): + conf = H2Conf(env) + conf.add_vhost_cgi(h2proxy_self=True) + conf.install() + assert env.apache_restart() == 0 + url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?body_error=timeout&error_bucket=0") + r = env.curl_get(url) + # depending on when the error is detect in proxying, if may RST the + # stream (exit_code != 0) or give a 503 response. + if r.exit_code == 0: + assert r.response['status'] == 503 diff --git a/test/modules/http2/test_700_load_get.py b/test/modules/http2/test_700_load_get.py new file mode 100644 index 0000000..9ee8898 --- /dev/null +++ b/test/modules/http2/test_700_load_get.py @@ -0,0 +1,63 @@ +import pytest + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +@pytest.mark.skipif(not H2TestEnv().h2load_is_at_least('1.41.0'), + reason="h2load misses --connect-to option") +class TestLoadGet: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(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_h2_700_10(self, env, start): + assert env.is_live() + text = "X" + chunk = 32 + for n in range(0, 5): + args = [env.h2load, "-n", "%d" % chunk, "-c", "1", "-m", "10", + f"--connect-to=localhost:{env.https_port}", + f"--base-uri={env.mkurl('https', 'cgi', '/')}", + ] + 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_h2_700_11(self, env, conns): + assert env.is_live() + text = "X" + start = 1200 + chunk = 64 + for n in range(0, 5): + args = [env.h2load, "-n", "%d" % chunk, "-c", "%d" % conns, "-m", "10", + f"--connect-to=localhost:{env.https_port}", + f"--base-uri={env.mkurl('https', 'cgi', '/')}", + ] + 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) diff --git a/test/modules/http2/test_710_load_post_static.py b/test/modules/http2/test_710_load_post_static.py new file mode 100644 index 0000000..ad8ae96 --- /dev/null +++ b/test/modules/http2/test_710_load_post_static.py @@ -0,0 +1,65 @@ +import pytest +import os + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestLoadPostStatic: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(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_h2_710_00(self, env, repeat): + assert env.is_live() + url = env.mkurl("https", "test1", "/index.html") + n = 10 + m = 1 + conn = 1 + fname = "data-10k" + args = [env.h2load, "-n", f"{n}", "-c", f"{conn}", "-m", f"{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_h2_710_01(self, env, repeat): + assert env.is_live() + url = env.mkurl("https", "test1", "/index.html") + n = 1000 + m = 100 + conn = 1 + fname = "data-1k" + args = [env.h2load, "-n", f"{n}", "-c", f"{conn}", "-m", f"{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_h2_710_02(self, env, repeat): + assert env.is_live() + url = env.mkurl("https", "test1", "/index.html") + n = 100 + m = 50 + conn = 1 + fname = "data-100k" + args = [env.h2load, "-n", f"{n}", "-c", f"{conn}", "-m", f"{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) diff --git a/test/modules/http2/test_711_load_post_cgi.py b/test/modules/http2/test_711_load_post_cgi.py new file mode 100644 index 0000000..82529d1 --- /dev/null +++ b/test/modules/http2/test_711_load_post_cgi.py @@ -0,0 +1,73 @@ +import pytest +import os + +from .env import H2Conf, H2TestEnv + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestLoadCgi: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + H2Conf(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_h2_711_10(self, env, repeat): + assert env.is_live() + 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_h2_711_11(self, env, repeat): + assert env.is_live() + 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_h2_711_12(self, env, repeat): + assert env.is_live() + 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) diff --git a/test/modules/http2/test_712_buffering.py b/test/modules/http2/test_712_buffering.py new file mode 100644 index 0000000..9eb2689 --- /dev/null +++ b/test/modules/http2/test_712_buffering.py @@ -0,0 +1,58 @@ +from datetime import timedelta + +import pytest + +from .env import H2Conf, H2TestEnv +from pyhttpd.curl import CurlPiper + + +@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here") +class TestBuffering: + + @pytest.fixture(autouse=True, scope='class') + def _class_scope(self, env): + conf = H2Conf(env) + conf.add_vhost_cgi(h2proxy_self=True).install() + assert env.apache_restart() == 0 + + @pytest.mark.skip(reason="this test shows unreliable jitter") + def test_h2_712_01(self, env): + # test gRPC like requests that do not end, but give answers, see #207 + # + # this test works like this: + # - use curl to POST data to the server /h2test/echo + # - feed curl the data in chunks, wait a bit between chunks + # - since some buffering on curl's stdout to Python is involved, + # we will see the response data only at the end. + # - therefore, we enable tracing with timestamps in curl on stderr + # and see when the response chunks arrive + # - if the server sends the incoming data chunks back right away, + # as it should, we see receiving timestamps separated roughly by the + # wait time between sends. + # + url = env.mkurl("https", "cgi", "/h2test/echo") + base_chunk = "0123456789" + chunks = ["chunk-{0:03d}-{1}\n".format(i, base_chunk) for i in range(5)] + stutter = timedelta(seconds=0.2) # this is short, but works on my machine (tm) + piper = CurlPiper(env=env, url=url) + piper.stutter_check(chunks, stutter) + + def test_h2_712_02(self, env): + # same as 712_01 but via mod_proxy_http2 + # + url = env.mkurl("https", "cgi", "/h2proxy/h2test/echo") + base_chunk = "0123456789" + chunks = ["chunk-{0:03d}-{1}\n".format(i, base_chunk) for i in range(3)] + stutter = timedelta(seconds=0.4) # need a bit more delay since we have the extra connection + piper = CurlPiper(env=env, url=url) + piper.stutter_check(chunks, stutter) + + def test_h2_712_03(self, env): + # same as 712_02 but with smaller chunks + # + url = env.mkurl("https", "cgi", "/h2proxy/h2test/echo") + base_chunk = "0" + chunks = ["ck{0}-{1}\n".format(i, base_chunk) for i in range(3)] + stutter = timedelta(seconds=0.4) # need a bit more delay since we have the extra connection + piper = CurlPiper(env=env, url=url) + piper.stutter_check(chunks, stutter) |