diff --git a/Makefile.am b/Makefile.am
index c0911b63..2b07b987 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -19,13 +19,8 @@ EXTRA_DIST = \
test/unit \
test/Makefile.in \
test/Makefile.am \
- test/mod_h2test/mod_h2test.c \
- test/mod_h2test/mod_h2test.h \
- test/conf \
- test/data \
- test/htdocs \
- test/*.py \
- test/*.ini.in
+ test/modules \
+ test/pyhttpd
dist_doc_DATA = README README.md LICENSE
diff --git a/test/modules/http2/conftest.py b/test/modules/http2/conftest.py
index 0cfa7cb4..80677743 100644
--- a/test/modules/http2/conftest.py
+++ b/test/modules/http2/conftest.py
@@ -10,7 +10,7 @@
def pytest_report_header(config, startdir):
- env = H2TestEnv(setup_dirs=False)
+ env = H2TestEnv()
return f"mod_h2 [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
@@ -36,6 +36,7 @@ def env(pytestconfig) -> H2TestEnv:
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
diff --git a/test/modules/http2/env.py b/test/modules/http2/env.py
index d3d0cafb..dae91657 100644
--- a/test/modules/http2/env.py
+++ b/test/modules/http2/env.py
@@ -16,15 +16,19 @@ 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"])
def make(self):
- super().make(add_modules=["http2", "proxy_http2"])
+ 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(self.env.local_dir, 'mod_h2test'))
+ cwd=os.path.join(local_dir, 'mod_h2test'))
rv = p.returncode
if rv != 0:
log.error(f"compiling md_h2test failed: {p.stderr}")
@@ -33,20 +37,34 @@ def _add_h2test(self):
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 \"{self.env.local_dir}/mod_h2test/.libs/mod_h2test.so\"\n")
+ 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):
- def __init__(self, pytestconfig=None, setup_dirs=True):
- super().__init__(pytestconfig=pytestconfig,
- local_dir=os.path.dirname(inspect.getfile(H2TestEnv)),
- add_base_conf=[
+ def __init__(self, pytestconfig=None):
+ super().__init__(pytestconfig=pytestconfig)
+ self.add_httpd_conf([
"H2MinWorkers 1",
"H2MaxWorkers 64",
"Protocols h2 http/1.1 h2c",
- ],
- interesting_modules=["http2", "proxy_http2", "h2test"])
+ ])
+ self.add_httpd_log_modules(["http2", "proxy_http2", "h2test"])
self.add_cert_specs([
CertificateSpec(domains=[
f"push.{self._http_tld}",
@@ -70,29 +88,12 @@ def __init__(self, pytestconfig=None, setup_dirs=True):
])
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:.*'),
+ re.compile(r'.*:tls_process_client_certificate:.*'),
])
- if setup_dirs:
- self._setup = H2TestSetup(env=self)
- self._setup.make()
- self.issue_certs()
- self.setup_data_1k_1m()
-
-
- def setup_data_1k_1m(self):
- s90 = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n"
- with open(os.path.join(self.gen_dir, "data-1k"), 'w') as f:
- for i in range(10):
- f.write(f"{i:09d}-{s90}")
- with open(os.path.join(self.gen_dir, "data-10k"), 'w') as f:
- for i in range(100):
- f.write(f"{i:09d}-{s90}")
- with open(os.path.join(self.gen_dir, "data-100k"), 'w') as f:
- for i in range(1000):
- f.write(f"{i:09d}-{s90}")
- with open(os.path.join(self.gen_dir, "data-1m"), 'w') as f:
- for i in range(10000):
- f.write(f"{i:09d}-{s90}")
+ def setup_httpd(self, setup: HttpdTestSetup = None):
+ super().setup_httpd(setup=H2TestSetup(env=self))
class H2Conf(HttpdConf):
@@ -105,7 +106,7 @@ def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None):
]
}))
- def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=False):
+ def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=None):
super().start_vhost(domains=domains, port=port, doc_root=doc_root, with_ssl=with_ssl)
if f"noh2.{self.env.http_tld}" in domains:
protos = ["http/1.1"]
@@ -133,4 +134,3 @@ def add_vhost_test1(self, proxy_self=False, h2proxy_self=False):
def add_vhost_test2(self):
return super().add_vhost_test2()
-
diff --git a/test/modules/http2/test_001_httpd_alive.py b/test/modules/http2/test_001_httpd_alive.py
index d922b440..557d17a4 100644
--- a/test/modules/http2/test_001_httpd_alive.py
+++ b/test/modules/http2/test_001_httpd_alive.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestBasicAlive:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_002_curl_basics.py b/test/modules/http2/test_002_curl_basics.py
index dfe39c53..bb03bd84 100644
--- a/test/modules/http2/test_002_curl_basics.py
+++ b/test/modules/http2/test_002_curl_basics.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestCurlBasics:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_003_get.py b/test/modules/http2/test_003_get.py
index a62da2ac..3fbaa081 100644
--- a/test/modules/http2/test_003_get.py
+++ b/test/modules/http2/test_003_get.py
@@ -4,7 +4,7 @@
from .env import H2Conf
-class TestStore:
+class TestGet:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
@@ -92,7 +92,7 @@ def test_h2_003_20(self, env):
Index of /006
-My Header Title
+Index of /006
- Parent Directory
- 006.css
- 006.js
diff --git a/test/modules/http2/test_004_post.py b/test/modules/http2/test_004_post.py
index 17ddd2a4..3e17b723 100644
--- a/test/modules/http2/test_004_post.py
+++ b/test/modules/http2/test_004_post.py
@@ -1,5 +1,6 @@
import difflib
import email.parser
+import inspect
import json
import os
import re
@@ -10,14 +11,17 @@
from .env import H2Conf
-class TestStore:
+class TestPost:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
- env.setup_data_1k_1m()
+ 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")
@@ -29,7 +33,7 @@ def curl_upload_and_verify(self, env, fname, options=None):
r2 = env.curl_get(r.response["header"]["location"])
assert r2.exit_code == 0
assert r2.response["status"] == 200
- with open(env.local_src(fpath), mode='rb') as file:
+ with open(self.local_src(fpath), mode='rb') as file:
src = file.read()
assert src == r2.response["body"]
@@ -84,7 +88,7 @@ def nghttp_post_and_verify(self, env, fname, options=None):
assert r.exit_code == 0
assert r.response["status"] >= 200 and r.response["status"] < 300
- with open(env.local_src(fpath), mode='rb') as file:
+ 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)
@@ -123,7 +127,7 @@ def nghttp_upload_and_verify(self, env, fname, options=None):
r2 = env.nghttp().get(r.response["header"]["location"])
assert r2.exit_code == 0
assert r2.response["status"] == 200
- with open(env.local_src(fpath), mode='rb') as file:
+ with open(self.local_src(fpath), mode='rb') as file:
src = file.read()
assert src == r2.response["body"]
@@ -204,7 +208,7 @@ def post_and_verify(fname, options=None):
if fname == part.get_filename():
filepart = part
assert filepart
- with open(env.local_src(fpath), mode='rb') as file:
+ with open(self.local_src(fpath), mode='rb') as file:
src = file.read()
assert src == filepart.get_payload(decode=True)
diff --git a/test/modules/http2/test_006_assets.py b/test/modules/http2/test_006_assets.py
index 44558da8..7a0222e0 100644
--- a/test/modules/http2/test_006_assets.py
+++ b/test/modules/http2/test_006_assets.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestAssets:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_100_conn_reuse.py b/test/modules/http2/test_100_conn_reuse.py
index c38cae81..7df7a605 100644
--- a/test/modules/http2/test_100_conn_reuse.py
+++ b/test/modules/http2/test_100_conn_reuse.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestConnReuse:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_101_ssl_reneg.py b/test/modules/http2/test_101_ssl_reneg.py
index 3aa96073..6d0b6ec7 100644
--- a/test/modules/http2/test_101_ssl_reneg.py
+++ b/test/modules/http2/test_101_ssl_reneg.py
@@ -4,7 +4,7 @@
from .env import H2Conf
-class TestStore:
+class TestSslRenegotiation:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_102_require.py b/test/modules/http2/test_102_require.py
index 1f0defa9..ed83a981 100644
--- a/test/modules/http2/test_102_require.py
+++ b/test/modules/http2/test_102_require.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestRequire:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_103_upgrade.py b/test/modules/http2/test_103_upgrade.py
index 3e96a288..92f9e364 100644
--- a/test/modules/http2/test_103_upgrade.py
+++ b/test/modules/http2/test_103_upgrade.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestUpgrade:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_104_padding.py b/test/modules/http2/test_104_padding.py
index 1bbff450..60915fab 100644
--- a/test/modules/http2/test_104_padding.py
+++ b/test/modules/http2/test_104_padding.py
@@ -8,7 +8,7 @@ def frame_padding(payload, padbits):
return ((payload + 9 + mask) & ~mask) - (payload + 9)
-class TestStore:
+class TestPadding:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_105_timeout.py b/test/modules/http2/test_105_timeout.py
index 558f6ec1..24133ae5 100644
--- a/test/modules/http2/test_105_timeout.py
+++ b/test/modules/http2/test_105_timeout.py
@@ -7,7 +7,7 @@
from pyhttpd.curl import CurlPiper
-class TestStore:
+class TestTimeout:
# Check that base servers 'Timeout' setting is observed on SSL handshake
def test_h2_105_01(self, env):
diff --git a/test/modules/http2/test_106_shutdown.py b/test/modules/http2/test_106_shutdown.py
index cf2a4d31..862656dc 100644
--- a/test/modules/http2/test_106_shutdown.py
+++ b/test/modules/http2/test_106_shutdown.py
@@ -40,6 +40,9 @@ def long_request():
assert env.apache_reload() == 0
t.join()
# noinspection PyTypeChecker
+ time.sleep(1)
r: ExecResult = self.r
- assert r.response["status"] == 200
- assert len(r.response["body"]) == (lines * (len(text)+1))
+ 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}"
diff --git a/test/modules/http2/test_200_header_invalid.py b/test/modules/http2/test_200_header_invalid.py
index c01bfca8..c1ac2dea 100644
--- a/test/modules/http2/test_200_header_invalid.py
+++ b/test/modules/http2/test_200_header_invalid.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestInvalidHeaders:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_201_header_conditional.py b/test/modules/http2/test_201_header_conditional.py
index 1e144a9f..be52400a 100644
--- a/test/modules/http2/test_201_header_conditional.py
+++ b/test/modules/http2/test_201_header_conditional.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestConditionalHeaders:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_202_trailer.py b/test/modules/http2/test_202_trailer.py
index c9081e00..57bcff77 100644
--- a/test/modules/http2/test_202_trailer.py
+++ b/test/modules/http2/test_202_trailer.py
@@ -13,7 +13,7 @@ def setup_data(env):
# The trailer tests depend on "nghttp" as no other client seems to be able to send those
# rare things.
-class TestStore:
+class TestTrailers:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_300_interim.py b/test/modules/http2/test_300_interim.py
index 3a2798bf..0ca674a0 100644
--- a/test/modules/http2/test_300_interim.py
+++ b/test/modules/http2/test_300_interim.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestInterimResponses:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_400_push.py b/test/modules/http2/test_400_push.py
index 29243b38..5c62c208 100644
--- a/test/modules/http2/test_400_push.py
+++ b/test/modules/http2/test_400_push.py
@@ -5,7 +5,7 @@
# The push tests depend on "nghttp"
-class TestStore:
+class TestPush:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_401_early_hints.py b/test/modules/http2/test_401_early_hints.py
index 2fc68ba6..fb099e5d 100644
--- a/test/modules/http2/test_401_early_hints.py
+++ b/test/modules/http2/test_401_early_hints.py
@@ -4,7 +4,7 @@
# The push tests depend on "nghttp"
-class TestStore:
+class TestEarlyHints:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_500_proxy.py b/test/modules/http2/test_500_proxy.py
index 3f287596..b626fdc8 100644
--- a/test/modules/http2/test_500_proxy.py
+++ b/test/modules/http2/test_500_proxy.py
@@ -1,3 +1,4 @@
+import inspect
import os
import re
import pytest
@@ -5,14 +6,17 @@
from .env import H2Conf
-class TestStore:
+class TestProxy:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
- env.setup_data_1k_1m()
+ TestProxy._local_dir = os.path.dirname(inspect.getfile(TestProxy))
H2Conf(env).add_vhost_cgi(proxy_self=True).install()
assert env.apache_restart() == 0
+ def local_src(self, fname):
+ return os.path.join(TestProxy._local_dir, fname)
+
def setup_method(self, method):
print("setup_method: %s" % method.__name__)
@@ -41,7 +45,7 @@ def curl_upload_and_verify(self, env, fname, options=None):
r2 = env.curl_get(re.sub(r'http:', 'https:', r.response["header"]["location"]))
assert r2.exit_code == 0
assert r2.response["status"] == 200
- with open(env.local_src(fpath), mode='rb') as file:
+ with open(self.local_src(fpath), mode='rb') as file:
src = file.read()
assert src == r2.response["body"]
@@ -58,7 +62,7 @@ def nghttp_post_and_verify(self, env, fname, options=None):
r = env.nghttp().upload(url, fpath, options=options)
assert r.exit_code == 0
assert 200 <= r.response["status"] < 300
- with open(env.local_src(fpath), mode='rb') as file:
+ with open(self.local_src(fpath), mode='rb') as file:
src = file.read()
assert src == r.response["body"]
@@ -88,7 +92,7 @@ def nghttp_upload_and_verify(self, env, fname, options=None):
r2 = env.nghttp().get(re.sub(r'http:', 'https:', r.response["header"]["location"]))
assert r2.exit_code == 0
assert r2.response["status"] == 200
- with open(env.local_src(fpath), mode='rb') as file:
+ with open(self.local_src(fpath), mode='rb') as file:
src = file.read()
assert src == r2.response["body"]
diff --git a/test/modules/http2/test_501_proxy_serverheader.py b/test/modules/http2/test_501_proxy_serverheader.py
index 322eb098..d78db1a6 100644
--- a/test/modules/http2/test_501_proxy_serverheader.py
+++ b/test/modules/http2/test_501_proxy_serverheader.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestProxyServerHeader:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_600_h2proxy.py b/test/modules/http2/test_600_h2proxy.py
index 17418580..1c0b7e96 100644
--- a/test/modules/http2/test_600_h2proxy.py
+++ b/test/modules/http2/test_600_h2proxy.py
@@ -3,11 +3,10 @@
from .env import H2Conf
-class TestStore:
+class TestH2Proxy:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
- env.setup_data_1k_1m()
conf = H2Conf(env)
conf.add_vhost_cgi(h2proxy_self=True)
if env.verbosity > 1:
diff --git a/test/modules/http2/test_700_load_get.py b/test/modules/http2/test_700_load_get.py
index e3127d58..d181cd28 100644
--- a/test/modules/http2/test_700_load_get.py
+++ b/test/modules/http2/test_700_load_get.py
@@ -3,7 +3,7 @@
from .env import H2Conf
-class TestStore:
+class TestLoadGet:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
diff --git a/test/modules/http2/test_710_load_post_static.py b/test/modules/http2/test_710_load_post_static.py
index aa7081cf..5e3ac1c8 100644
--- a/test/modules/http2/test_710_load_post_static.py
+++ b/test/modules/http2/test_710_load_post_static.py
@@ -4,11 +4,10 @@
from .env import H2Conf
-class TestStore:
+class TestLoadPostStatic:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
- env.setup_data_1k_1m()
H2Conf(env).add_vhost_test1().install()
assert env.apache_restart() == 0
diff --git a/test/modules/http2/test_711_load_post_cgi.py b/test/modules/http2/test_711_load_post_cgi.py
index 94cfec3d..dca9ce5f 100644
--- a/test/modules/http2/test_711_load_post_cgi.py
+++ b/test/modules/http2/test_711_load_post_cgi.py
@@ -4,11 +4,10 @@
from .env import H2Conf
-class TestStore:
+class TestLoadCgi:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
- env.setup_data_1k_1m()
H2Conf(env).add_vhost_cgi(proxy_self=True, h2proxy_self=True).install()
assert env.apache_restart() == 0
diff --git a/test/modules/http2/test_712_buffering.py b/test/modules/http2/test_712_buffering.py
index e0fcea89..21931481 100644
--- a/test/modules/http2/test_712_buffering.py
+++ b/test/modules/http2/test_712_buffering.py
@@ -10,7 +10,6 @@ class TestBuffering:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
- env.setup_data_1k_1m()
conf = H2Conf(env)
conf.add_vhost_cgi(h2proxy_self=True).install()
assert env.apache_restart() == 0
diff --git a/test/pyhttpd/certs.py b/test/pyhttpd/certs.py
index 2ce93c07..5519f161 100644
--- a/test/pyhttpd/certs.py
+++ b/test/pyhttpd/certs.py
@@ -70,13 +70,24 @@ def name(self) -> Optional[str]:
return self.domains[0]
return None
+ @property
+ def type(self) -> Optional[str]:
+ if self.domains and len(self.domains):
+ return "server"
+ elif self.client:
+ return "client"
+ elif self.name:
+ return "ca"
+ return None
+
class Credentials:
- def __init__(self, name: str, cert: Any, pkey: Any):
+ def __init__(self, name: str, cert: Any, pkey: Any, issuer: 'Credentials' = None):
self._name = name
self._cert = cert
self._pkey = pkey
+ self._issuer = issuer
self._cert_file = None
self._pkey_file = None
self._store = None
@@ -117,6 +128,10 @@ def pkey_pem(self) -> bytes:
PrivateFormat.TraditionalOpenSSL if self.key_type.startswith('rsa') else PrivateFormat.PKCS8,
NoEncryption())
+ @property
+ def issuer(self) -> Optional['Credentials']:
+ return self._issuer
+
def set_store(self, store: 'CertStore'):
self._store = store
@@ -145,13 +160,17 @@ def issue_certs(self, specs: List[CertificateSpec],
def issue_cert(self, spec: CertificateSpec, chain: List['Credentials'] = None) -> 'Credentials':
key_type = spec.key_type if spec.key_type else self.key_type
- creds = self._store.load_credentials(name=spec.name, key_type=key_type, single_file=spec.single_file) \
- if self._store else None
+ creds = None
+ if self._store:
+ creds = self._store.load_credentials(
+ name=spec.name, key_type=key_type, single_file=spec.single_file, issuer=self)
if creds is None:
creds = HttpdTestCA.create_credentials(spec=spec, issuer=self, key_type=key_type,
valid_from=spec.valid_from, valid_to=spec.valid_to)
if self._store:
self._store.save(creds, single_file=spec.single_file)
+ if spec.type == "ca":
+ self._store.save_chain(creds, "ca", with_root=True)
if spec.sub_specs:
if self._store:
@@ -196,6 +215,19 @@ def save(self, creds: Credentials, name: str = None,
creds.set_files(cert_file, pkey_file)
self._add_credentials(name, creds)
+ def save_chain(self, creds: Credentials, infix: str, with_root=False):
+ name = creds.name
+ chain = [creds]
+ while creds.issuer is not None:
+ creds = creds.issuer
+ chain.append(creds)
+ if not with_root and len(chain) > 1:
+ chain = chain[:-1]
+ chain_file = os.path.join(self._store_dir, f'{name}-{infix}.pem')
+ with open(chain_file, "wb") as fd:
+ for c in chain:
+ fd.write(c.cert_pem)
+
def _add_credentials(self, name: str, creds: Credentials):
if name not in self._creds_by_name:
self._creds_by_name[name] = []
@@ -220,13 +252,13 @@ def load_pem_pkey(self, fpath: str):
with open(fpath) as fd:
return load_pem_private_key("".join(fd.readlines()).encode(), password=None)
- def load_credentials(self, name: str, key_type=None, single_file: bool = False):
+ def load_credentials(self, name: str, key_type=None, single_file: bool = False, issuer: Credentials = None):
cert_file = self.get_cert_file(name=name, key_type=key_type)
pkey_file = cert_file if single_file else self.get_pkey_file(name=name, key_type=key_type)
if os.path.isfile(cert_file) and os.path.isfile(pkey_file):
cert = self.load_pem_cert(cert_file)
pkey = self.load_pem_pkey(pkey_file)
- creds = Credentials(name=name, cert=cert, pkey=pkey)
+ creds = Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer)
creds.set_store(self)
creds.set_files(cert_file, pkey_file)
self._add_credentials(name, creds)
@@ -239,7 +271,7 @@ class HttpdTestCA:
@classmethod
def create_root(cls, name: str, store_dir: str, key_type: str = "rsa2048") -> Credentials:
store = CertStore(fpath=store_dir)
- creds = store.load_credentials(name="ca", key_type=key_type)
+ creds = store.load_credentials(name="ca", key_type=key_type, issuer=None)
if creds is None:
creds = HttpdTestCA._make_ca_credentials(name=name, key_type=key_type)
store.save(creds, name="ca")
@@ -405,7 +437,7 @@ def _make_ca_credentials(name, key_type: Any,
cert = csr.sign(private_key=issuer_key,
algorithm=hashes.SHA256(),
backend=default_backend())
- return Credentials(name=name, cert=cert, pkey=pkey)
+ return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer)
@staticmethod
def _make_server_credentials(name: str, domains: List[str], issuer: Credentials,
@@ -423,7 +455,7 @@ def _make_server_credentials(name: str, domains: List[str], issuer: Credentials,
cert = csr.sign(private_key=issuer.private_key,
algorithm=hashes.SHA256(),
backend=default_backend())
- return Credentials(name=name, cert=cert, pkey=pkey)
+ return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer)
@staticmethod
def _make_client_credentials(name: str,
@@ -441,4 +473,4 @@ def _make_client_credentials(name: str,
cert = csr.sign(private_key=issuer.private_key,
algorithm=hashes.SHA256(),
backend=default_backend())
- return Credentials(name=name, cert=cert, pkey=pkey)
+ return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer)
diff --git a/test/pyhttpd/conf.py b/test/pyhttpd/conf.py
index ee4c3006..3fefffaa 100644
--- a/test/pyhttpd/conf.py
+++ b/test/pyhttpd/conf.py
@@ -40,23 +40,25 @@ def add_certificate(self, cert_file, key_file):
if self.env.ssl_module == "ssl":
self.add([
f"SSLCertificateFile {cert_file}",
- f"SSLCertificateKeyFile {key_file}",
+ f"SSLCertificateKeyFile {key_file if key_file else cert_file}",
])
elif self.env.ssl_module == "tls":
self.add(f"""
TLSCertificate {cert_file} {key_file}
""")
- def add_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=True):
+ def add_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=None):
self.start_vhost(domains=domains, port=port, doc_root=doc_root, with_ssl=with_ssl)
self.end_vhost()
return self
- def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=False):
+ def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=None):
if not isinstance(domains, list):
domains = [domains]
if port is None:
port = self.env.https_port
+ if with_ssl is None:
+ with_ssl = (self.env.https_port == port)
self.add("")
self.add(f"")
self._indents += 1
@@ -64,7 +66,7 @@ def start_vhost(self, domains, port=None, doc_root="htdocs", with_ssl=False):
for alias in domains[1:]:
self.add(f"ServerAlias {alias}")
self.add(f"DocumentRoot {doc_root}")
- if self.env.https_port == port or with_ssl:
+ if with_ssl:
if self.env.ssl_module == "ssl":
self.add("SSLEngine on")
for cred in self.env.get_credentials_for_name(domains[0]):
@@ -104,7 +106,6 @@ def add_vhost_test1(self, proxy_self=False, h2proxy_self=False):
self.add([
"",
" Options +Indexes",
- " HeaderName /006/header.html",
"",
])
self.add_proxies("test1", proxy_self, h2proxy_self)
@@ -121,7 +122,6 @@ def add_vhost_test2(self):
self.add([
"",
" Options +Indexes",
- " HeaderName /006/header.html",
"",
])
self.end_vhost()
diff --git a/test/pyhttpd/conf/stop.conf.template b/test/pyhttpd/conf/stop.conf.template
new file mode 100644
index 00000000..21bae845
--- /dev/null
+++ b/test/pyhttpd/conf/stop.conf.template
@@ -0,0 +1,46 @@
+# a config safe to use for stopping the server
+# this allows us to stop the server even when+
+# the config in the file is borked (as test cases may try to do that)
+#
+ServerName localhost
+ServerRoot "${server_dir}"
+
+Include "conf/modules.conf"
+
+DocumentRoot "${server_dir}/htdocs"
+
+
+ LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\" %k" combined
+ LogFormat "%h %l %u %t \"%r\" %>s %b" common
+ CustomLog "logs/access_log" combined
+
+
+
+TypesConfig "${gen_dir}/apache/conf/mime.types"
+
+Listen ${http_port}
+Listen ${https_port}
+
+
+ # provide some default
+ SSLSessionCache "shmcb:ssl_gcache_data(32000)"
+
+
+
+ ServerName ${http_tld}
+ ServerAlias www.${http_tld}
+
+ SSLEngine off
+
+ DocumentRoot "${server_dir}/htdocs"
+
+
+
+ Options Indexes FollowSymLinks
+ AllowOverride None
+ Require all granted
+
+ AddHandler cgi-script .py
+ AddHandler cgi-script .cgi
+ Options +ExecCGI
+
diff --git a/test/pyhttpd/env.py b/test/pyhttpd/env.py
index bb974195..73044ae4 100644
--- a/test/pyhttpd/env.py
+++ b/test/pyhttpd/env.py
@@ -35,7 +35,6 @@ class HttpdTestSetup:
"logio",
"unixd",
"version",
- "watchdog",
"authn_core",
"authz_host",
"authz_groupfile",
@@ -56,32 +55,35 @@ class HttpdTestSetup:
"setenvif",
"slotmem_shm",
"status",
- "autoindex",
- "cgid",
"dir",
"alias",
"rewrite",
"deflate",
"proxy",
"proxy_http",
- "proxy_balancer",
- "proxy_hcheck",
]
def __init__(self, env: 'HttpdTestEnv'):
self.env = env
+ self._source_dirs = [os.path.dirname(inspect.getfile(HttpdTestSetup))]
+ self._modules = HttpdTestSetup.MODULES.copy()
- def make(self, modules: List[str] = None, add_modules: List[str] = None):
+ def add_source_dir(self, source_dir):
+ self._source_dirs.append(source_dir)
+
+ def add_modules(self, modules: List[str]):
+ for m in modules:
+ if m not in self._modules:
+ self._modules.append(m)
+
+ def make(self):
self._make_dirs()
self._make_conf()
- mod_names = modules.copy() if modules else self.MODULES.copy()
- if add_modules:
- mod_names.extend(add_modules)
- if self.env.mpm_module is not None and self.env.mpm_module not in mod_names:
- mod_names.append(self.env.mpm_module)
- if self.env.ssl_module is not None and self.env.ssl_module not in mod_names:
- mod_names.append(self.env.ssl_module)
- self._make_modules_conf(modules=mod_names)
+ if self.env.mpm_module is not None:
+ self.add_modules([self.env.mpm_module])
+ if self.env.ssl_module is not None:
+ self.add_modules([self.env.ssl_module])
+ self._make_modules_conf()
self._make_htdocs()
self.env.clear_curl_headerfiles()
@@ -93,18 +95,22 @@ def _make_dirs(self):
os.makedirs(self.env.server_logs_dir)
def _make_conf(self):
- our_dir = os.path.dirname(inspect.getfile(Dummy))
- conf_src_dir = os.path.join(our_dir, 'conf')
+ # remove anything from another run/test suite
conf_dest_dir = os.path.join(self.env.server_dir, 'conf')
- if not os.path.exists(conf_dest_dir):
- os.makedirs(conf_dest_dir)
- for name in os.listdir(conf_src_dir):
- src_path = os.path.join(conf_src_dir, name)
- m = re.match(r'(.+).template', name)
- if m:
- self._make_template(src_path, os.path.join(conf_dest_dir, m.group(1)))
- elif os.path.isfile(src_path):
- shutil.copy(src_path, os.path.join(conf_dest_dir, name))
+ if os.path.isdir(conf_dest_dir):
+ shutil.rmtree(conf_dest_dir)
+ for d in self._source_dirs:
+ conf_src_dir = os.path.join(d, 'conf')
+ if os.path.isdir(conf_src_dir):
+ if not os.path.exists(conf_dest_dir):
+ os.makedirs(conf_dest_dir)
+ for name in os.listdir(conf_src_dir):
+ src_path = os.path.join(conf_src_dir, name)
+ m = re.match(r'(.+).template', name)
+ if m:
+ self._make_template(src_path, os.path.join(conf_dest_dir, m.group(1)))
+ elif os.path.isfile(src_path):
+ shutil.copy(src_path, os.path.join(conf_dest_dir, name))
def _make_template(self, src, dest):
var_map = dict()
@@ -115,12 +121,12 @@ def _make_template(self, src, dest):
with open(dest, 'w') as fd:
fd.write(t.substitute(var_map))
- def _make_modules_conf(self, modules: List[str]):
+ def _make_modules_conf(self):
modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf')
with open(modules_conf, 'w') as fd:
# issue load directives for all modules we want that are shared
missing_mods = list()
- for m in modules:
+ for m in self._modules:
mod_path = os.path.join(self.env.libexec_dir, f"mod_{m}.so")
if os.path.isfile(mod_path):
fd.write(f"LoadModule {m}_module \"{mod_path}\"\n")
@@ -133,18 +139,23 @@ def _make_modules_conf(self, modules: List[str]):
f"DSOs: {self.env.dso_modules}")
def _make_htdocs(self):
- our_dir = os.path.dirname(inspect.getfile(Dummy))
if not os.path.exists(self.env.server_docs_dir):
os.makedirs(self.env.server_docs_dir)
- shutil.copytree(os.path.join(our_dir, 'htdocs'),
- os.path.join(self.env.server_dir, 'htdocs'),
- dirs_exist_ok=True)
- cgi_dir = os.path.join(self.env.server_dir, 'htdocs/cgi')
- for name in os.listdir(cgi_dir):
- if re.match(r'.+\.py', name):
- cgi_file = os.path.join(cgi_dir, name)
- st = os.stat(cgi_file)
- os.chmod(cgi_file, st.st_mode | stat.S_IEXEC)
+ dest_dir = os.path.join(self.env.server_dir, 'htdocs')
+ # remove anything from another run/test suite
+ if os.path.isdir(dest_dir):
+ shutil.rmtree(dest_dir)
+ for d in self._source_dirs:
+ srcdocs = os.path.join(d, 'htdocs')
+ if os.path.isdir(srcdocs):
+ shutil.copytree(srcdocs, dest_dir, dirs_exist_ok=True)
+ # make all contained .py scripts executable
+ for dirpath, _dirnames, filenames in os.walk(dest_dir):
+ for fname in filenames:
+ if re.match(r'.+\.py', fname):
+ py_file = os.path.join(dirpath, fname)
+ st = os.stat(py_file)
+ os.chmod(py_file, st.st_mode | stat.S_IEXEC)
class HttpdTestEnv:
@@ -153,11 +164,8 @@ class HttpdTestEnv:
def get_ssl_module(cls):
return os.environ['SSL'] if 'SSL' in os.environ else 'ssl'
- def __init__(self, pytestconfig=None,
- local_dir=None, add_base_conf: List[str] = None,
- interesting_modules: List[str] = None):
+ def __init__(self, pytestconfig=None):
self._our_dir = os.path.dirname(inspect.getfile(Dummy))
- self._local_dir = local_dir if local_dir else self._our_dir
self.config = ConfigParser(interpolation=ExtendedInterpolation())
self.config.read(os.path.join(self._our_dir, 'config.ini'))
@@ -196,24 +204,12 @@ def __init__(self, pytestconfig=None,
self._http_base = f"http://{self._httpd_addr}:{self.http_port}"
self._https_base = f"https://{self._httpd_addr}:{self.https_port}"
+ self._verbosity = pytestconfig.option.verbose if pytestconfig is not None else 0
self._test_conf = os.path.join(self._server_conf_dir, "test.conf")
self._httpd_base_conf = []
- if add_base_conf:
- self._httpd_base_conf.extend(add_base_conf)
-
- self._verbosity = pytestconfig.option.verbose if pytestconfig is not None else 0
- if self._verbosity >= 2:
- log_level = "trace2"
- self._httpd_base_conf .append(f"LogLevel core:trace5 {self.mpm_module}:trace5")
- elif self._verbosity >= 1:
- log_level = "debug"
- else:
- log_level = "info"
- if interesting_modules:
- l = "LogLevel"
- for name in interesting_modules:
- l += f" {name}:{log_level}"
- self._httpd_base_conf.append(l)
+ self._httpd_log_modules = []
+ self._log_interesting = None
+ self._setup = None
self._ca = None
self._cert_specs = [CertificateSpec(domains=[
@@ -226,6 +222,35 @@ def __init__(self, pytestconfig=None,
self._verify_certs = False
self._curl_headerfiles_n = 0
+ def add_httpd_conf(self, lines: List[str]):
+ self._httpd_base_conf.extend(lines)
+
+ def add_httpd_log_modules(self, modules: List[str]):
+ self._httpd_log_modules.extend(modules)
+
+ def issue_certs(self):
+ if self._ca is None:
+ self._ca = HttpdTestCA.create_root(name=self.http_tld,
+ store_dir=os.path.join(self.server_dir, 'ca'),
+ key_type="rsa4096")
+ self._ca.issue_certs(self._cert_specs)
+
+ def setup_httpd(self, setup: HttpdTestSetup = None):
+ """Create the server environment with config, htdocs and certificates"""
+ self._setup = setup if setup is not None else HttpdTestSetup(env=self)
+ self._setup.make()
+ self.issue_certs()
+ if self._httpd_log_modules:
+ if self._verbosity >= 2:
+ log_level = "trace2"
+ elif self._verbosity >= 1:
+ log_level = "debug"
+ else:
+ log_level = "info"
+ self._log_interesting = "LogLevel"
+ for name in self._httpd_log_modules:
+ self._log_interesting += f" {name}:{log_level}"
+
@property
def apxs(self) -> str:
return self._apxs
@@ -282,10 +307,6 @@ def bin_dir(self) -> str:
def gen_dir(self) -> str:
return self._gen_dir
- @property
- def local_dir(self) -> str:
- return self._local_dir
-
@property
def test_dir(self) -> str:
return self._test_dir
@@ -318,17 +339,10 @@ def server_conf_dir(self) -> str:
def server_docs_dir(self) -> str:
return self._server_docs_dir
- @property
- def httpd_base_conf(self) -> List[str]:
- return self._httpd_base_conf
-
@property
def httpd_error_log(self) -> HttpdErrorLog:
return self._error_log
- def local_src(self, path):
- return os.path.join(self.local_dir, path)
-
def htdocs_src(self, path):
return os.path.join(self._our_dir, 'htdocs', path)
@@ -347,15 +361,8 @@ def apachectl_stderr(self):
def add_cert_specs(self, specs: List[CertificateSpec]):
self._cert_specs.extend(specs)
- def issue_certs(self):
- if self._ca is None:
- self._ca = HttpdTestCA.create_root(name=self.http_tld,
- store_dir=os.path.join(self.server_dir, 'ca'),
- key_type="rsa4096")
- self._ca.issue_certs(self._cert_specs)
-
def get_credentials_for_name(self, dns_name) -> List['Credentials']:
- for spec in self._cert_specs:
+ for spec in [s for s in self._cert_specs if s.domains is not None]:
if dns_name in spec.domains:
return self.ca.get_credentials_for_name(spec.domains[0])
return []
@@ -396,12 +403,12 @@ def mkpath(self, path):
if not os.path.exists(path):
return os.makedirs(path)
- def run(self, args, input=None, debug_log=True):
+ def run(self, args, intext=None, debug_log=True):
if debug_log:
log.debug(f"run: {args}")
start = datetime.now()
p = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE,
- input=input.encode() if input else None)
+ input=intext.encode() if intext else None)
return ExecResult(args=args, exit_code=p.returncode,
stdout=p.stdout, stderr=p.stderr,
duration=datetime.now() - start)
@@ -414,6 +421,11 @@ def install_test_conf(self, lines: List[str]):
with open(self._test_conf, 'w') as fd:
fd.write('\n'.join(self._httpd_base_conf))
fd.write('\n')
+ if self._verbosity >= 2:
+ fd.write(f"LogLevel core:trace5 {self.mpm_module}:trace5\n")
+ if self._log_interesting:
+ fd.write(self._log_interesting)
+ fd.write('\n\n')
fd.write('\n'.join(lines))
fd.write('\n')
@@ -427,7 +439,7 @@ def is_live(self, url: str = None, timeout: timedelta = None):
while datetime.now() < try_until:
# noinspection PyBroadException
try:
- r = self.curl_get(url, insecure=True, debug_log=False)
+ r = self.curl_get(url, insecure=True)
if r.exit_code == 0:
return True
time.sleep(.1)
@@ -452,7 +464,7 @@ def is_dead(self, url: str = None, timeout: timedelta = None):
while datetime.now() < try_until:
# noinspection PyBroadException
try:
- r = self.curl_get(url, debug_log=False)
+ r = self.curl_get(url)
if r.exit_code != 0:
return True
time.sleep(.1)
@@ -468,9 +480,10 @@ def is_dead(self, url: str = None, timeout: timedelta = None):
return False
def _run_apachectl(self, cmd) -> ExecResult:
+ conf_file = 'stop.conf' if cmd == 'stop' else 'httpd.conf'
args = [self._apachectl,
"-d", self.server_dir,
- "-f", os.path.join(self._server_dir, 'conf/httpd.conf'),
+ "-f", os.path.join(self._server_dir, f'conf/{conf_file}'),
"-k", cmd]
r = self.run(args)
self._apachectl_stderr = r.stderr
@@ -596,7 +609,7 @@ def curl_parse_headerfile(self, headerfile: str, r: ExecResult = None) -> ExecRe
return r
def curl_raw(self, urls, timeout=10, options=None, insecure=False,
- debug_log=True, force_resolve=True):
+ force_resolve=True):
xopt = ['-vvvv']
if options:
xopt.extend(options)
@@ -611,9 +624,8 @@ def curl_raw(self, urls, timeout=10, options=None, insecure=False,
os.remove(headerfile)
return r
- def curl_get(self, url, insecure=False, debug_log=True, options=None):
- return self.curl_raw([url], insecure=insecure,
- options=options, debug_log=debug_log)
+ def curl_get(self, url, insecure=False, options=None):
+ return self.curl_raw([url], insecure=insecure, options=options)
def curl_upload(self, url, fpath, timeout=5, options=None):
if not options: