]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
the big pytest test suite sync with trunk
authorStefan Eissing <icing@apache.org>
Thu, 11 Jul 2024 08:18:12 +0000 (08:18 +0000)
committerStefan Eissing <icing@apache.org>
Thu, 11 Jul 2024 08:18:12 +0000 (08:18 +0000)
all the good stuff collected over the aeons now
also in the 2.4.x maintenance branch

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1919126 13f79535-47bb-0310-9956-ffa450edef68

67 files changed:
test/modules/core/conftest.py
test/modules/core/env.py [new file with mode: 0644]
test/modules/core/test_001_encoding.py
test/modules/core/test_002_restarts.py [new file with mode: 0644]
test/modules/http1/__init__.py [new file with mode: 0644]
test/modules/http1/conftest.py [new file with mode: 0644]
test/modules/http1/env.py [new file with mode: 0644]
test/modules/http1/htdocs/cgi/files/empty.txt [new file with mode: 0644]
test/modules/http1/htdocs/cgi/hello.py [new file with mode: 0755]
test/modules/http1/htdocs/cgi/requestparser.py [new file with mode: 0644]
test/modules/http1/htdocs/cgi/upload.py [new file with mode: 0755]
test/modules/http1/mod_h1test/mod_h1test.c [new file with mode: 0644]
test/modules/http1/mod_h1test/mod_h1test.slo [new file with mode: 0644]
test/modules/http1/test_001_alive.py [new file with mode: 0644]
test/modules/http1/test_003_get.py [new file with mode: 0644]
test/modules/http1/test_004_post.py [new file with mode: 0644]
test/modules/http1/test_005_trailers.py [new file with mode: 0644]
test/modules/http1/test_006_unsafe.py [new file with mode: 0644]
test/modules/http1/test_007_strict.py [new file with mode: 0644]
test/modules/http2/conftest.py
test/modules/http2/env.py
test/modules/http2/test_007_ssi.py
test/modules/http2/test_008_ranges.py
test/modules/http2/test_100_conn_reuse.py
test/modules/http2/test_101_ssl_reneg.py
test/modules/http2/test_102_require.py
test/modules/http2/test_103_upgrade.py
test/modules/http2/test_105_timeout.py
test/modules/http2/test_106_shutdown.py
test/modules/http2/test_200_header_invalid.py
test/modules/http2/test_203_rfc9113.py
test/modules/http2/test_500_proxy.py
test/modules/http2/test_600_h2proxy.py
test/modules/http2/test_700_load_get.py
test/modules/http2/test_712_buffering.py
test/modules/http2/test_800_websockets.py
test/modules/md/conftest.py
test/modules/md/test_300_conf_validate.py
test/modules/md/test_702_auto.py
test/modules/md/test_720_wildcard.py
test/modules/md/test_730_static.py
test/modules/md/test_740_acme_errors.py
test/modules/md/test_741_setup_errors.py
test/modules/md/test_750_eab.py
test/modules/md/test_780_tailscale.py
test/modules/md/test_790_failover.py
test/modules/md/test_900_notify.py
test/modules/md/test_901_message.py
test/modules/md/test_920_status.py
test/modules/proxy/conftest.py
test/modules/proxy/env.py
test/modules/proxy/test_02_unix.py
test/modules/tls/conf.py
test/modules/tls/conftest.py
test/modules/tls/env.py
test/modules/tls/test_02_conf.py
test/modules/tls/test_03_sni.py
test/modules/tls/test_06_ciphers.py
test/modules/tls/test_08_vars.py
test/modules/tls/test_14_proxy_ssl.py
test/modules/tls/test_15_proxy_tls.py
test/modules/tls/test_16_proxy_mixed.py
test/modules/tls/test_17_proxy_machine_cert.py
test/pyhttpd/conf.py
test/pyhttpd/curl.py
test/pyhttpd/env.py
test/pyhttpd/log.py

index 439cd22e171b6b274044537da5113e236bd92317..22906efbb040551dbbe552f7023849ce0f638e36 100644 (file)
@@ -4,41 +4,27 @@ import os
 import pytest
 import sys
 
+from .env import CoreTestEnv
 from pyhttpd.env import HttpdTestEnv
 
 sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
 
 
 def pytest_report_header(config, startdir):
-    env = HttpdTestEnv()
+    env = CoreTestEnv()
     return f"core [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
 
 
 @pytest.fixture(scope="package")
-def env(pytestconfig) -> HttpdTestEnv:
+def env(pytestconfig) -> CoreTestEnv:
     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 = HttpdTestEnv(pytestconfig=pytestconfig)
+    env = CoreTestEnv(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):
-    env.httpd_error_log.set_ignored_lognos([
-        'AH10244',  # core: invalid URI path
-        'AH01264',  # mod_cgid script not found
-    ])
-    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/core/env.py b/test/modules/core/env.py
new file mode 100644 (file)
index 0000000..9c63380
--- /dev/null
@@ -0,0 +1,25 @@
+import inspect
+import logging
+import os
+
+from pyhttpd.env import HttpdTestEnv, HttpdTestSetup
+
+log = logging.getLogger(__name__)
+
+
+class CoreTestSetup(HttpdTestSetup):
+
+    def __init__(self, env: 'HttpdTestEnv'):
+        super().__init__(env=env)
+        self.add_source_dir(os.path.dirname(inspect.getfile(CoreTestSetup)))
+        self.add_modules(["cgid"])
+
+
+class CoreTestEnv(HttpdTestEnv):
+
+    def __init__(self, pytestconfig=None):
+        super().__init__(pytestconfig=pytestconfig)
+        self.add_httpd_log_modules(["http", "core"])
+
+    def setup_httpd(self, setup: HttpdTestSetup = None):
+        super().setup_httpd(setup=CoreTestSetup(env=self))
index b7ffbaa84295292786f8d809c8e4fcc9cf8e4401..a3b24d047105822318a1b78a4c94e902c23e4111 100644 (file)
@@ -1,12 +1,11 @@
 import pytest
+from typing import List, Optional
 
 from pyhttpd.conf import HttpdConf
 
 
 class TestEncoding:
 
-    EXP_AH10244_ERRS = 0
-
     @pytest.fixture(autouse=True, scope='class')
     def _class_scope(self, env):
         conf = HttpdConf(env, extras={
@@ -57,29 +56,29 @@ class TestEncoding:
         assert r.response["status"] == 200
 
     # check path traversals
-    @pytest.mark.parametrize(["path", "status"], [
-        ["/../echo.py", 400],
-        ["/nothing/../../echo.py", 400],
-        ["/cgi-bin/../../echo.py", 400],
-        ["/nothing/%2e%2e/%2e%2e/echo.py", 400],
-        ["/cgi-bin/%2e%2e/%2e%2e/echo.py", 400],
-        ["/nothing/%%32%65%%32%65/echo.py", 400],
-        ["/cgi-bin/%%32%65%%32%65/echo.py", 400],
-        ["/nothing/%%32%65%%32%65/%%32%65%%32%65/h2_env.py", 400],
-        ["/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/h2_env.py", 400],
-        ["/nothing/%25%32%65%25%32%65/echo.py", 404],
-        ["/cgi-bin/%25%32%65%25%32%65/echo.py", 404],
-        ["/nothing/%25%32%65%25%32%65/%25%32%65%25%32%65/h2_env.py", 404],
-        ["/cgi-bin/%25%32%65%25%32%65/%25%32%65%25%32%65/h2_env.py", 404],
+    @pytest.mark.parametrize(["path", "status", "lognos"], [
+        ["/../echo.py", 400, ["AH10244"]],
+        ["/nothing/../../echo.py", 400, ["AH10244"]],
+        ["/cgi-bin/../../echo.py", 400, ["AH10244"]],
+        ["/nothing/%2e%2e/%2e%2e/echo.py", 400, ["AH10244"]],
+        ["/cgi-bin/%2e%2e/%2e%2e/echo.py", 400, ["AH10244"]],
+        ["/nothing/%%32%65%%32%65/echo.py", 400, ["AH10244"]],
+        ["/cgi-bin/%%32%65%%32%65/echo.py", 400, ["AH10244"]],
+        ["/nothing/%%32%65%%32%65/%%32%65%%32%65/h2_env.py", 400, ["AH10244"]],
+        ["/cgi-bin/%%32%65%%32%65/%%32%65%%32%65/h2_env.py", 400, ["AH10244"]],
+        ["/nothing/%25%32%65%25%32%65/echo.py", 404, ["AH01264"]],
+        ["/cgi-bin/%25%32%65%25%32%65/echo.py", 404, ["AH01264"]],
+        ["/nothing/%25%32%65%25%32%65/%25%32%65%25%32%65/h2_env.py", 404, ["AH01264"]],
+        ["/cgi-bin/%25%32%65%25%32%65/%25%32%65%25%32%65/h2_env.py", 404, ["AH01264"]],
     ])
-    def test_core_001_04(self, env, path, status):
+    def test_core_001_04(self, env, path, status, lognos: Optional[List[str]]):
         url = env.mkurl("https", "test1", path)
         r = env.curl_get(url)
         assert r.response["status"] == status
-        if status == 400:
-            TestEncoding.EXP_AH10244_ERRS += 1
-            # the log will have a core:err about invalid URI path
-
+        #
+        if lognos is not None:
+            env.httpd_error_log.ignore_recent(lognos = lognos)
     # check handling of %2f url encodings that are not decoded by default
     @pytest.mark.parametrize(["host", "path", "status"], [
         ["test1", "/006%2f006.css", 404],
diff --git a/test/modules/core/test_002_restarts.py b/test/modules/core/test_002_restarts.py
new file mode 100644 (file)
index 0000000..cf203bc
--- /dev/null
@@ -0,0 +1,150 @@
+import os
+import re
+import time
+from datetime import datetime, timedelta
+from threading import Thread
+
+import pytest
+
+from .env import CoreTestEnv
+from pyhttpd.conf import HttpdConf
+
+
+class Loader:
+
+    def __init__(self, env, url: str, clients: int, req_per_client: int = 10):
+        self.env = env
+        self.url = url
+        self.clients = clients
+        self.req_per_client = req_per_client
+        self.result = None
+        self.total_request = 0
+        self._thread = None
+
+    def run(self):
+        self.total_requests = self.clients * self.req_per_client
+        conn_per_client = 5
+        args = [self.env.h2load, f"--connect-to=localhost:{self.env.https_port}",
+                "--h1",                                 # use only http/1.1
+                "-n", str(self.total_requests),              # total # of requests to make
+                "-c", str(conn_per_client * self.clients),   # total # of connections to make
+                "-r", str(self.clients),                     # connections at a time
+                "--rate-period", "2",                   # create conns every 2 sec
+                self.url,
+                ]
+        self.result = self.env.run(args)
+
+    def start(self):
+        self._thread = Thread(target=self.run)
+        self._thread.start()
+
+    def join(self):
+        self._thread.join()
+
+
+class ChildDynamics:
+
+    RE_DATE_TIME = re.compile(r'\[(?P<date_time>[^\]]+)\] .*')
+    RE_TIME_FRAC = re.compile(r'(?P<dt>.* \d\d:\d\d:\d\d)(?P<frac>.(?P<micros>.\d+)) (?P<year>\d+)')
+    RE_CHILD_CHANGE = re.compile(r'\[(?P<date_time>[^\]]+)\] '
+                                 r'\[mpm_event:\w+\]'
+                                 r' \[pid (?P<main_pid>\d+):tid \w+\] '
+                                 r'.* Child (?P<child_no>\d+) (?P<action>\w+): '
+                                 r'pid (?P<pid>\d+), gen (?P<generation>\d+), .*')
+
+    def __init__(self, env: CoreTestEnv):
+        self.env = env
+        self.changes = list()
+        self._start = None
+        for l in open(env.httpd_error_log.path):
+            m = self.RE_CHILD_CHANGE.match(l)
+            if m:
+                self.changes.append({
+                    'pid': int(m.group('pid')),
+                    'child_no': int(m.group('child_no')),
+                    'gen': int(m.group('generation')),
+                    'action': m.group('action'),
+                    'rtime' : self._rtime(m.group('date_time'))
+                })
+                continue
+            if self._start is None:
+                m = self.RE_DATE_TIME.match(l)
+                if m:
+                    self._rtime(m.group('date_time'))
+
+    def _rtime(self, s: str) -> timedelta:
+        micros = 0
+        m = self.RE_TIME_FRAC.match(s)
+        if m:
+            micros = int(m.group('micros'))
+            s = f"{m.group('dt')} {m.group('year')}"
+        d = datetime.strptime(s, '%a %b %d %H:%M:%S %Y') + timedelta(microseconds=micros)
+        if self._start is None:
+            self._start = d
+        delta = d - self._start
+        return f"{delta.seconds:+02d}.{delta.microseconds:06d}"
+
+
+
+@pytest.mark.skipif(condition='STRESS_TEST' not in os.environ,
+                    reason="STRESS_TEST not set in env")
+@pytest.mark.skipif(condition=not CoreTestEnv().h2load_is_at_least('1.41.0'),
+                    reason="h2load unavailable or misses --connect-to option")
+class TestRestarts:
+
+    def test_core_002_01(self, env):
+        # Lets make a tight config that triggers dynamic child behaviour
+        conf = HttpdConf(env, extras={
+            'base': f"""
+        StartServers            1
+        ServerLimit             3
+        ThreadLimit             4
+        ThreadsPerChild         4
+        MinSpareThreads         4
+        MaxSpareThreads         6
+        MaxRequestWorkers       12
+        MaxConnectionsPerChild  0
+
+        LogLevel mpm_event:trace6
+                """,
+        })
+        conf.add_vhost_cgi()
+        conf.install()
+
+        # clear logs and start server, start load
+        env.httpd_error_log.clear_log()
+        assert env.apache_restart() == 0
+        # we should see a single child started
+        cd = ChildDynamics(env)
+        assert len(cd.changes) == 1, f"{cd.changes}"
+        assert cd.changes[0]['action'] == 'started'
+        # This loader simulates 6 clients, each making 10 requests.
+        # delay.py sleeps for 1sec, so this should run for about 10 seconds
+        loader = Loader(env=env, url=env.mkurl("https", "cgi", "/delay.py"),
+                        clients=6, req_per_client=10)
+        loader.start()
+        # Expect 2 more children to have been started after half time
+        time.sleep(5)
+        cd = ChildDynamics(env)
+        assert len(cd.changes) == 3, f"{cd.changes}"
+        assert len([x for x in cd.changes if x['action'] == 'started']) == 3, f"{cd.changes}"
+
+        # Trigger a server reload
+        assert env.apache_reload() == 0
+        # a graceful reload lets ongoing requests continue, but
+        # after a while all gen 0 children should have stopped
+        time.sleep(3)  # FIXME: this pbly depends on the runtime a lot, do we have expectations?
+        cd = ChildDynamics(env)
+        gen0 = [x for x in cd.changes if x['gen'] == 0]
+        assert len([x for x in gen0 if x['action'] == 'stopped']) == 3
+
+        # wait for the loader to finish and stop the server
+        loader.join()
+        env.apache_stop()
+
+        # Similar to before the reload, we expect 3 children to have
+        # been started and stopped again on server stop
+        cd = ChildDynamics(env)
+        gen1 = [x for x in cd.changes if x['gen'] == 1]
+        assert len([x for x in gen1 if x['action'] == 'started']) == 3
+        assert len([x for x in gen1 if x['action'] == 'stopped']) == 3
diff --git a/test/modules/http1/__init__.py b/test/modules/http1/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/modules/http1/conftest.py b/test/modules/http1/conftest.py
new file mode 100644 (file)
index 0000000..33a16a1
--- /dev/null
@@ -0,0 +1,36 @@
+import logging
+import os
+
+import pytest
+import sys
+
+sys.path.append(os.path.join(os.path.dirname(__file__), '../..'))
+
+from .env import H1TestEnv
+
+
+def pytest_report_header(config, startdir):
+    env = H1TestEnv()
+    return f"mod_http [apache: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
+
+
+def pytest_generate_tests(metafunc):
+    if "repeat" in metafunc.fixturenames:
+        count = int(metafunc.config.getoption("repeat"))
+        metafunc.fixturenames.append('tmp_ct')
+        metafunc.parametrize('repeat', range(count))
+
+
+@pytest.fixture(scope="package")
+def env(pytestconfig) -> H1TestEnv:
+    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 = H1TestEnv(pytestconfig=pytestconfig)
+    env.setup_httpd()
+    env.apache_access_log_clear()
+    env.httpd_error_log.clear_log()
+    return env
diff --git a/test/modules/http1/env.py b/test/modules/http1/env.py
new file mode 100644 (file)
index 0000000..e2df1a5
--- /dev/null
@@ -0,0 +1,81 @@
+import inspect
+import logging
+import os
+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 H1TestSetup(HttpdTestSetup):
+
+    def __init__(self, env: 'HttpdTestEnv'):
+        super().__init__(env=env)
+        self.add_source_dir(os.path.dirname(inspect.getfile(H1TestSetup)))
+        self.add_modules(["cgid", "autoindex", "ssl"])
+
+    def make(self):
+        super().make()
+        self._add_h1test()
+        self._setup_data_1k_1m()
+
+    def _add_h1test(self):
+        local_dir = os.path.dirname(inspect.getfile(H1TestSetup))
+        p = subprocess.run([self.env.apxs, '-c', 'mod_h1test.c'],
+                           capture_output=True,
+                           cwd=os.path.join(local_dir, 'mod_h1test'))
+        rv = p.returncode
+        if rv != 0:
+            log.error(f"compiling md_h1test failed: {p.stderr}")
+            raise Exception(f"compiling md_h1test 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 h1test_module   \"{local_dir}/mod_h1test/.libs/mod_h1test.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 H1TestEnv(HttpdTestEnv):
+
+    def __init__(self, pytestconfig=None):
+        super().__init__(pytestconfig=pytestconfig)
+        self.add_httpd_log_modules(["http", "core"])
+
+    def setup_httpd(self, setup: HttpdTestSetup = None):
+        super().setup_httpd(setup=H1TestSetup(env=self))
+
+
+class H1Conf(HttpdConf):
+
+    def __init__(self, env: HttpdTestEnv, extras: Dict[str, Any] = None):
+        super().__init__(env=env, extras=HttpdConf.merge_extras(extras, {
+            "base": [
+                "LogLevel http:trace4",
+            ],
+            f"cgi.{env.http_tld}": [
+                "SSLOptions +StdEnvVars",
+                "AddHandler cgi-script .py",
+                "<Location \"/h1test/echo\">",
+                "    SetHandler h1test-echo",
+                "</Location>",
+            ]
+        }))
diff --git a/test/modules/http1/htdocs/cgi/files/empty.txt b/test/modules/http1/htdocs/cgi/files/empty.txt
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/modules/http1/htdocs/cgi/hello.py b/test/modules/http1/htdocs/cgi/hello.py
new file mode 100755 (executable)
index 0000000..191acb2
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+
+import os
+
+print("Content-Type: application/json")
+print()
+print("{")
+print("  \"https\" : \"%s\"," % (os.getenv('HTTPS', '')))
+print("  \"host\" : \"%s\"," % (os.getenv('SERVER_NAME', '')))
+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("}")
+
diff --git a/test/modules/http1/htdocs/cgi/requestparser.py b/test/modules/http1/htdocs/cgi/requestparser.py
new file mode 100644 (file)
index 0000000..c7e0648
--- /dev/null
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+import os
+import sys
+from urllib import parse
+import multipart # https://github.com/andrew-d/python-multipart (`apt install python3-multipart`)
+import shutil
+
+
+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
+
+
+class FileItem:
+
+    def __init__(self, mparse_item):
+        self.item = mparse_item
+
+    @property
+    def file_name(self):
+        return os.path.basename(self.item.file_name.decode())
+
+    def save_to(self, destpath: str):
+        fsrc = self.item.file_object
+        fsrc.seek(0)
+        with open(destpath, 'wb') as fd:
+            shutil.copyfileobj(fsrc, fd)
+
+
+def get_request_params():
+    oforms = {}
+    ofiles = {}
+    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]
+    if "CONTENT_TYPE" in os.environ:
+        ctype = os.environ["CONTENT_TYPE"]
+        if ctype == "application/x-www-form-urlencoded":
+            s = sys.stdin.read()
+            qforms = parse.parse_qs(s)
+            for name, values in qforms.items():
+                oforms[name] = values[0]
+        elif ctype.startswith("multipart/"):
+            def on_field(field):
+                oforms[field.field_name.decode()] = field.value.decode()
+            def on_file(file):
+                ofiles[file.field_name.decode()] = FileItem(file)
+            multipart.parse_form(headers={"Content-Type": ctype},
+                                 input_stream=sys.stdin.buffer,
+                                 on_field=on_field, on_file=on_file)
+    return oforms, ofiles
+
diff --git a/test/modules/http1/htdocs/cgi/upload.py b/test/modules/http1/htdocs/cgi/upload.py
new file mode 100755 (executable)
index 0000000..632b7e9
--- /dev/null
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+import os
+import sys
+from requestparser import get_request_params
+
+
+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.file_name
+    fpath = f'{os.environ["DOCUMENT_ROOT"]}/files/{fname}'
+    fitem.save_to(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/http1/mod_h1test/mod_h1test.c b/test/modules/http1/mod_h1test/mod_h1test.c
new file mode 100644 (file)
index 0000000..cbd87b5
--- /dev/null
@@ -0,0 +1,129 @@
+/* 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>
+
+static void h1test_hooks(apr_pool_t *pool);
+
+AP_DECLARE_MODULE(h1test) = {
+    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 */
+    h1test_hooks,
+#if defined(AP_MODULE_FLAG_NONE)
+    AP_MODULE_FLAG_ALWAYS_MERGE
+#endif
+};
+
+
+static int h1test_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, "h1test-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;
+    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, "h1test-trailers-in",
+                       apr_itoa(r->pool, 1));
+    }
+    if (apr_table_get(r->headers_in, "Add-Trailer")) {
+        ap_log_rerror(APLOG_MARK, APLOG_TRACE2, 0, r,
+                      "echo_handler: seeing incoming Add-Trailer header");
+        apr_table_setn(r->trailers_out, "h1test-add-trailer",
+                       apr_table_get(r->headers_in, "Add-Trailer"));
+    }
+
+    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, "h1test_echo_handler failed");
+        return AP_FILTER_ERROR;
+    }
+    return DECLINED;
+}
+
+
+/* Install this module into the apache2 infrastructure.
+ */
+static void h1test_hooks(apr_pool_t *pool)
+{
+    ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool, "installing hooks and handlers");
+
+    /* test h1 handlers */
+    ap_hook_handler(h1test_echo_handler, NULL, NULL, APR_HOOK_MIDDLE);
+}
+
diff --git a/test/modules/http1/mod_h1test/mod_h1test.slo b/test/modules/http1/mod_h1test/mod_h1test.slo
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/test/modules/http1/test_001_alive.py b/test/modules/http1/test_001_alive.py
new file mode 100644 (file)
index 0000000..0a1de1d
--- /dev/null
@@ -0,0 +1,20 @@
+import pytest
+
+from .env import H1Conf
+
+
+class TestBasicAlive:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        H1Conf(env).add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    # we expect to see the document from the generic server
+    def test_h1_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/http1/test_003_get.py b/test/modules/http1/test_003_get.py
new file mode 100644 (file)
index 0000000..1cd5917
--- /dev/null
@@ -0,0 +1,27 @@
+import socket
+
+import pytest
+
+from .env import H1Conf
+
+
+class TestGet:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        H1Conf(env).add_vhost_cgi(
+            proxy_self=True
+        ).add_vhost_test1(
+            proxy_self=True
+        ).install()
+        assert env.apache_restart() == 0
+
+    # check SSL environment variables from CGI script
+    def test_h1_003_01(self, env):
+        url = env.mkurl("https", "cgi", "/hello.py")
+        r = env.curl_get(url)
+        assert r.response["status"] == 200
+        assert r.response["json"]["protocol"] == "HTTP/1.1"
+        assert r.response["json"]["https"] == "on"
+        tls_version = r.response["json"]["ssl_protocol"]
+        assert tls_version in ["TLSv1.2", "TLSv1.3"]
diff --git a/test/modules/http1/test_004_post.py b/test/modules/http1/test_004_post.py
new file mode 100644 (file)
index 0000000..005a8c2
--- /dev/null
@@ -0,0 +1,53 @@
+import difflib
+import email.parser
+import inspect
+import json
+import os
+import sys
+
+import pytest
+
+from .env import H1Conf
+
+
+class TestPost:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        TestPost._local_dir = os.path.dirname(inspect.getfile(TestPost))
+        H1Conf(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"]
+        return r
+
+    def test_h1_004_01(self, env):
+        self.curl_upload_and_verify(env, "data-1k", ["-vvv"])
+
+    def test_h1_004_02(self, env):
+        self.curl_upload_and_verify(env, "data-10k", [])
+
+    def test_h1_004_03(self, env):
+        self.curl_upload_and_verify(env, "data-100k", [])
+
+    def test_h1_004_04(self, env):
+        self.curl_upload_and_verify(env, "data-1m", [])
+
+    def test_h1_004_05(self, env):
+        r = self.curl_upload_and_verify(env, "data-1k", ["-vvv", "-H", "Expect: 100-continue"])
diff --git a/test/modules/http1/test_005_trailers.py b/test/modules/http1/test_005_trailers.py
new file mode 100644 (file)
index 0000000..ca717a0
--- /dev/null
@@ -0,0 +1,42 @@
+import os
+import pytest
+
+from .env import H1Conf
+
+
+# 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):
+        H1Conf(env).add_vhost_cgi(proxy_self=True).install()
+        assert env.apache_restart() == 0
+
+    # check that we get a trailer out when telling the handler to add one
+    def test_h1_005_01(self, env):
+        if not env.httpd_is_at_least("2.5.0"):
+            pytest.skip(f'need at least httpd 2.5.0 for this')
+        url = env.mkurl("https", "cgi", "/h1test/echo")
+        host = f"cgi.{env.http_tld}"
+        fpath = os.path.join(env.gen_dir, "data-1k")
+        r = env.curl_upload(url, fpath, options=["--header", "Add-Trailer: 005_01"])
+        assert r.exit_code == 0, f"{r}"
+        assert 200 <= r.response["status"] < 300
+        assert r.response["trailer"], f"no trailers received: {r}"
+        assert "h1test-add-trailer" in r.response["trailer"]
+        assert r.response["trailer"]["h1test-add-trailer"] == "005_01"
+
+    # check that we get out trailers through the proxy
+    def test_h1_005_02(self, env):
+        if not env.httpd_is_at_least("2.5.0"):
+            pytest.skip(f'need at least httpd 2.5.0 for this')
+        url = env.mkurl("https", "cgi", "/proxy/h1test/echo")
+        host = f"cgi.{env.http_tld}"
+        fpath = os.path.join(env.gen_dir, "data-1k")
+        r = env.curl_upload(url, fpath, options=["--header", "Add-Trailer: 005_01"])
+        assert r.exit_code == 0, f"{r}"
+        assert 200 <= r.response["status"] < 300
+        assert r.response["trailer"], f"no trailers received: {r}"
+        assert "h1test-add-trailer" in r.response["trailer"]
+        assert r.response["trailer"]["h1test-add-trailer"] == "005_01"
diff --git a/test/modules/http1/test_006_unsafe.py b/test/modules/http1/test_006_unsafe.py
new file mode 100644 (file)
index 0000000..eb83217
--- /dev/null
@@ -0,0 +1,134 @@
+import re
+import socket
+from typing import List, Optional
+
+import pytest
+
+from .env import H1Conf
+
+class TestRequestUnsafe:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        conf = H1Conf(env)
+        conf.add([
+            "HttpProtocolOptions Unsafe",
+        ])
+        conf.install()
+        assert env.apache_restart() == 0
+
+    # unsafe tests from t/apache/http_strict.t
+    # possible expected results:
+    #   0:       any HTTP error
+    #   1:       any HTTP success
+    #   200-500: specific HTTP status code
+    #   None:   HTTPD should drop connection without error message
+    @pytest.mark.parametrize(["intext", "status", "lognos"], [
+        ["GET / HTTP/1.0\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\n\n", 1, None],
+        ["get / HTTP/1.0\r\n\r\n", 501, ["AH00135"]],
+        ["G ET / HTTP/1.0\r\n\r\n", 400, None],
+        ["G\0ET / HTTP/1.0\r\n\r\n", 400, None],
+        ["G/T / HTTP/1.0\r\n\r\n", 501, ["AH00135"]],
+        ["GET /\0 HTTP/1.0\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\0\r\n\r\n", 400, None],
+        ["GET\f/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET\r/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET\t/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET / HTT/1.0\r\n\r\n", 0, None],
+        ["GET / HTTP/1.0\r\nHost: localhost\r\n\r\n", 1, None],
+        ["GET / HTTP/2.0\r\nHost: localhost\r\n\r\n", 1, None],
+        ["GET / HTTP/1.2\r\nHost: localhost\r\n\r\n", 1, None],
+        ["GET / HTTP/1.11\r\nHost: localhost\r\n\r\n", 400, None],
+        ["GET / HTTP/10.0\r\nHost: localhost\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0  \r\nHost: localhost\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0 x\r\nHost: localhost\r\n\r\n", 400, None],
+        ["GET / HTTP/\r\nHost: localhost\r\n\r\n", 0, None],
+        ["GET / HTTP/0.9\r\n\r\n", 0, None],
+        ["GET / HTTP/0.8\r\n\r\n", 0, None],
+        ["GET /\x01 HTTP/1.0\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nFoo: bar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nFoo:bar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nFoo: b\0ar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nFoo: b\x01ar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nFoo\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nFoo bar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\n: bar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nX: bar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nFoo bar:bash\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nFoo :bar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\n Foo:bar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nF\x01o: bar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nF\ro: bar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nF\to: bar\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nFo: b\tar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nFo: bar\r\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\r", None, None],
+        ["GET /\r\n", 0, None],
+        ["GET /#frag HTTP/1.0\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nHost: localhost\r\nHost: localhost\r\n\r\n", 200, None],
+        ["GET http://017700000001/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET http://0x7f.1/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET http://127.0.0.1/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET http://127.01.0.1/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET http://%3127.0.0.1/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nHost: localhost:80\r\nHost: localhost:80\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nHost: localhost:80 x\r\n\r", 400, None],
+        ["GET http://localhost:80/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET http://localhost:80x/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET http://localhost:80:80/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET http://localhost::80/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET http://foo@localhost:80/ HTTP/1.0\r\n\r\n", 200, None],
+        ["GET http://[::1]/ HTTP/1.0\r\n\r\n", 1, None],
+        ["GET http://[::1:2]/ HTTP/1.0\r\n\r\n", 1, None],
+        ["GET http://[4712::abcd]/ HTTP/1.0\r\n\r\n", 1, None],
+        ["GET http://[4712::abcd:1]/ HTTP/1.0\r\n\r\n", 1, None],
+        ["GET http://[4712::abcd::]/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET http://[4712:abcd::,]/ HTTP/1.0\r\n\r\n", 1, None],
+        ["GET http://[4712::abcd]:8000/ HTTP/1.0\r\n\r\n", 1, None],
+        ["GET http://4713::abcd:8001/ HTTP/1.0\r\n\r\n", 400, None],
+        ["GET / HTTP/1.0\r\nHost: [::1]\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\r\nHost: [::1:2]\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\r\nHost: [4711::abcd]\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\r\nHost: [4711::abcd:1]\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\r\nHost: [4711:abcd::]\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\r\nHost: [4711::abcd]:8000\r\n\r\n", 1, None],
+        ["GET / HTTP/1.0\r\nHost: 4714::abcd:8001\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nHost: abc\xa0\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nHost: abc\\foo\r\n\r\n", 400, None],
+        ["GET http://foo/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+        ["GET http://foo:81/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+        ["GET http://[::1]:81/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+        ["GET http://10.0.0.1:81/ HTTP/1.0\r\nHost: bar\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nHost: foo-bar.example.com\r\n\r\n", 200, None],
+        ["GET / HTTP/1.0\r\nHost: foo_bar.example.com\r\n\r\n", 200, None],
+        ["GET http://foo_bar/ HTTP/1.0\r\n\r\n", 200, None],
+    ])
+    def test_h1_006_01(self, env, intext, status: Optional[int], lognos: Optional[List[str]]):
+        with socket.create_connection(('localhost', int(env.http_port))) as sock:
+            # on some OS, the server does not see our connection until there is
+            # something incoming
+            sock.sendall(intext.encode())
+            sock.shutdown(socket.SHUT_WR)
+            buff = sock.recv(1024)
+            msg = buff.decode()
+            if status is None:
+                assert len(msg) == 0, f"unexpected answer: {msg}"
+            else:
+                assert len(msg) > 0, "no answer from server"
+                rlines = msg.splitlines()
+                response = rlines[0]
+                m = re.match(r'^HTTP/1.1 (\d+)\s+(\S+)', response)
+                assert m or status == 0, f"unrecognized response: {rlines}"
+                if status == 1:
+                    assert int(m.group(1)) >= 200
+                elif status == 0:
+                    # headerless 0.9 response, yuk
+                    assert len(rlines) >= 1, f"{rlines}"
+                elif status > 0:
+                    assert int(m.group(1)) == status, f"{rlines}"
+                else:
+                    assert int(m.group(1)) >= 400, f"{rlines}"
+                #
+                if lognos is not None:
+                    env.httpd_error_log.ignore_recent(lognos = lognos)
diff --git a/test/modules/http1/test_007_strict.py b/test/modules/http1/test_007_strict.py
new file mode 100644 (file)
index 0000000..7c52f68
--- /dev/null
@@ -0,0 +1,126 @@
+import re
+import socket
+from typing import List, Optional
+
+import pytest
+
+from .env import H1Conf
+
+
+class TestRequestStrict:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        conf = H1Conf(env)
+        conf.add([
+            "HttpProtocolOptions Strict",
+        ])
+        conf.install()
+        assert env.apache_restart() == 0
+
+    # strict tests from t/apache/http_strict.t
+    # possible expected results:
+    #   0:       any HTTP error
+    #   1:       any HTTP success
+    #   200-500: specific HTTP status code
+    #   undef:   HTTPD should drop connection without error message
+    @pytest.mark.parametrize(["intext", "status"], [
+        ["GET / HTTP/1.0\n\n", 400],
+        ["G/T / HTTP/1.0\r\n\r\n", 400],
+        ["GET / HTTP/1.0  \r\nHost: localhost\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\nFoo: b\x01ar\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\nF\x01o: bar\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\r", None],
+        ["GET / HTTP/1.0\r\nHost: localhost\r\nHost: localhost\r\n\r\n", 400],
+        ["GET http://017700000001/ HTTP/1.0\r\n\r\n", 400],
+        ["GET http://0x7f.1/ HTTP/1.0\r\n\r\n", 400],
+        ["GET http://127.01.0.1/ HTTP/1.0\r\n\r\n", 400],
+        ["GET http://%3127.0.0.1/ HTTP/1.0\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\nHost: localhost:80\r\nHost: localhost:80\r\n\r\n", 400],
+        ["GET http://foo@localhost:80/ HTTP/1.0\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\nHost: 4714::abcd:8001\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\nHost: abc\xa0\r\n\r\n", 400],
+        ["GET / HTTP/1.0\r\nHost: foo_bar.example.com\r\n\r\n", 200],
+        ["GET http://foo_bar/ HTTP/1.0\r\n\r\n", 200],
+    ])
+    def test_h1_007_01(self, env, intext, status: Optional[int]):
+        with socket.create_connection(('localhost', int(env.http_port))) as sock:
+            # on some OS, the server does not see our connection until there is
+            # something incoming
+            sock.sendall(intext.encode())
+            sock.shutdown(socket.SHUT_WR)
+            buff = sock.recv(1024)
+            msg = buff.decode()
+            if status is None:
+                assert len(msg) == 0, f"unexpected answer: {msg}"
+            else:
+                assert len(msg) > 0, "no answer from server"
+                rlines = msg.splitlines()
+                response = rlines[0]
+                m = re.match(r'^HTTP/1.1 (\d+)\s+(\S+)', response)
+                assert m, f"unrecognized response: {rlines}"
+                if status == 1:
+                    assert int(m.group(1)) >= 200
+                elif status == 90:
+                    assert len(rlines) >= 1, f"{rlines}"
+                elif status > 0:
+                    assert int(m.group(1)) == status, f"{rlines}"
+                else:
+                    assert int(m.group(1)) >= 400, f"{rlines}"
+
+    @pytest.mark.parametrize(["hvalue", "expvalue", "status", "lognos"], [
+        ['"123"', '123', 200, None],
+        ['"123 "', '123 ', 200, None],       # trailing space stays
+        ['"123\t"', '123\t', 200, None],     # trailing tab stays
+        ['" 123"', '123', 200, None],        # leading space is stripped
+        ['"          123"', '123', 200, None],  # leading spaces are stripped
+        ['"\t123"', '123', 200, None],       # leading tab is stripped
+        ['"expr=%{unescape:123%0A 123}"', '', 500, ["AH02430"]],  # illegal char
+        ['" \t "', '', 200, None],           # just ws
+    ])
+    def test_h1_007_02(self, env, hvalue, expvalue, status, lognos: Optional[List[str]]):
+        hname = 'ap-test-007'
+        conf = H1Conf(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=['--http1.1'])
+        assert r.response["status"] == status
+        if int(status) < 400:
+            assert r.response["header"][hname] == expvalue
+        #
+        if lognos is not None:
+            env.httpd_error_log.ignore_recent(lognos = lognos)
+
+    @pytest.mark.parametrize(["hvalue", "expvalue"], [
+        ['123', '123'],
+        ['123 ', '123'],    # trailing space is stripped
+        ['123\t', '123'],    # trailing tab is stripped
+        [' 123', '123'],    # leading space is stripped
+        ['          123', '123'],  # leading spaces are stripped
+        ['\t123', '123'],  # leading tab is stripped
+    ])
+    def test_h1_007_03(self, env, hvalue, expvalue):
+        # same as 007_02, but http1 proxied
+        hname = 'ap-test-007'
+        conf = H1Conf(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", "/proxy/index.html")
+        r = env.curl_get(url, options=['--http1.1'])
+        assert r.response["status"] == 200
+        assert r.response["header"][hname] == expvalue
index 55d0c3a21e3041f12474f7339ce6997d953e9ef0..118cef1a9501578826e2afefece306c67d4a563d 100644 (file)
@@ -30,11 +30,10 @@ def env(pytestconfig) -> H2TestEnv:
 
 
 @pytest.fixture(autouse=True, scope="package")
-def _session_scope(env):
+def _h2_package_scope(env):
+    env.httpd_error_log.add_ignored_lognos([
+        'AH10400',  # warning that 'enablereuse' has not effect in certain configs
+        'AH00045',  # child did not exit in time, SIGTERM was sent
+    ])
     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))
-
index 34d196d6bd63a6aa99a730070a087efcab9f024a..b2443e003b658fc3207587934492ae1918f6c5eb 100644 (file)
@@ -1,8 +1,8 @@
 import inspect
 import logging
 import os
-import re
 import subprocess
+from shutil import copyfile
 from typing import Dict, Any
 
 from pyhttpd.certs import CertificateSpec
@@ -53,6 +53,12 @@ class H2TestSetup(HttpdTestSetup):
         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}")
+        test1_docs = os.path.join(self.env.server_docs_dir, 'test1')
+        self.env.mkpath(test1_docs)
+        for fname in ["data-1k", "data-10k", "data-100k", "data-1m"]:
+            src = os.path.join(self.env.gen_dir, fname)
+            dest = os.path.join(test1_docs, fname)
+            copyfile(src, dest)
 
 
 class H2TestEnv(HttpdTestEnv):
@@ -85,34 +91,6 @@ class H2TestEnv(HttpdTestEnv):
             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
-            'AH00045',  # child did not exit in time, SIGTERM was sent
-        ])
-        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))
 
index 97e38df031295a57dbe3f18309be2f3b95231cf7..f5411bccd4371f08509b3d2d92729bbd24ad387b 100644 (file)
@@ -1,4 +1,3 @@
-import re
 import pytest
 
 from .env import H2Conf, H2TestEnv
index 4dcdcc8ccb1f4dd5a359e7b022b15e43514ee9ea..dd695bb08d1ec0a03369a91fc8c0f90dab2c45b0 100644 (file)
@@ -1,13 +1,16 @@
 import inspect
 import json
+import logging
 import os
 import re
-import time
 import pytest
 
 from .env import H2Conf, H2TestEnv
 
 
+log = logging.getLogger(__name__)
+
+
 @pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestRanges:
 
@@ -123,13 +126,17 @@ class TestRanges:
             '--limit-rate', '2k', '-m', '2'
         ])
         assert r.exit_code != 0, f'{r}'
+        # Restart for logs to be flushed out
+        assert env.apache_restart() == 0
         found = False
         for line in open(TestRanges.LOGFILE).readlines():
             e = json.loads(line)
+            log.info(f'inspecting logged request: {e["request"]}')
             if e['request'] == f'GET {path}?03broken HTTP/2.0':
                 assert e['bytes_rx_I'] > 0
                 assert e['bytes_resp_B'] == 100*1024*1024
                 assert e['bytes_tx_O'] > 1024
+                assert e['bytes_tx_O'] < 100*1024*1024  # curl buffers, but not that much
                 found = True
                 break
         assert found, f'request not found in {self.LOGFILE}'
@@ -141,18 +148,13 @@ class TestRanges:
         assert env.apache_restart() == 0
         stats = self.get_server_status(env)
         # we see the server uptime check request here
-        assert 1 == int(stats['Total Accesses']), f'{stats}'
-        assert 1 == int(stats['Total kBytes']), f'{stats}'
+        assert 1 == int(stats['Total Accesses'])
+        assert 1 == int(stats['Total kBytes'])
         count = 10
         url = env.mkurl("https", "test1", f'/data-100m?[0-{count-1}]')
         r = env.curl_get(url, 5, options=['--http2', '-H', f'Range: bytes=0-{4096}'])
         assert r.exit_code == 0, f'{r}'
-        for _ in range(10):
-            # slow cpu might not success on first read
-            stats = self.get_server_status(env)
-            if (4*count)+1 <= int(stats['Total kBytes']):
-                break
-            time.sleep(0.1)
+        stats = self.get_server_status(env)
         # amount reported is larger than (count *4k), the net payload
         # but does not exceed an additional 4k
         assert (4*count)+1 <= int(stats['Total kBytes'])
index 3ebac24d60bf2cdd54de2f0bb9129c0077b1dc2f..103166fa3019d6077ccf9e1794f8ef3640bf40fc 100644 (file)
@@ -48,6 +48,12 @@ class TestConnReuse:
         hostname = ("noh2.%s" % env.http_tld)
         r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ])
         assert 421 == r.response["status"]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02032"   # Hostname provided via SNI and hostname provided via HTTP have no compatible SSL setup
+            ]
+        )
 
     # access an unknown vhost, after using ServerName in SNI
     def test_h2_100_05(self, env):
@@ -55,3 +61,9 @@ class TestConnReuse:
         hostname = ("unknown.%s" % env.http_tld)
         r = env.curl_get(url, 5, options=[ "-H", "Host:%s" % hostname ])
         assert 421 == r.response["status"]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02032"   # Hostname provided via SNI and hostname provided via HTTP have no compatible SSL setup
+            ]
+        )
index 528002fb88af8ef0c865068db074544218c3c19a..d278af21ed456bc8d8fb5f54f1e0f5f1da252bc7 100644 (file)
@@ -56,6 +56,12 @@ class TestSslRenegotiation:
         assert 0 == r.exit_code, f"{r}"
         assert r.response
         assert 403 == r.response["status"]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01276" # No matching DirectoryIndex found
+            ]
+        )
         
     # try to renegotiate the cipher, should fail with correct code
     def test_h2_101_02(self, env):
@@ -68,6 +74,16 @@ class TestSslRenegotiation:
         assert 0 != r.exit_code
         assert not r.response
         assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02261"   # Re-negotiation handshake failed
+            ],
+            matches = [
+                r'.*:tls_post_process_client_hello:.*',
+                r'.*SSL Library Error:.*:SSL routines::no shared cipher.*'
+            ]
+        )
         
     # try to renegotiate a client certificate from Location 
     # needs to fail with correct code
@@ -79,6 +95,16 @@ class TestSslRenegotiation:
         assert 0 != r.exit_code
         assert not r.response
         assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02261"   # Re-negotiation handshake failed
+            ],
+            matches = [
+                r'.*:tls_process_client_certificate:.*',
+                r'.*SSL Library Error:.*:SSL routines::peer did not return a certificate.*'
+            ]
+        )
         
     # try to renegotiate a client certificate from Directory 
     # needs to fail with correct code
@@ -90,6 +116,16 @@ class TestSslRenegotiation:
         assert 0 != r.exit_code, f"{r}"
         assert not r.response
         assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02261"   # Re-negotiation handshake failed
+            ],
+            matches = [
+                r'.*:tls_process_client_certificate:.*',
+                r'.*SSL Library Error:.*:SSL routines::peer did not return a certificate.*'
+            ]
+        )
         
     # make 10 requests on the same connection, none should produce a status code
     # reported by erki@example.ee
@@ -136,3 +172,13 @@ class TestSslRenegotiation:
         assert 0 != r.exit_code
         assert not r.response
         assert re.search(r'HTTP_1_1_REQUIRED \(err 13\)', r.stderr)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02261"   # Re-negotiation handshake failed
+            ],
+            matches = [
+                r'.*:tls_post_process_client_hello:.*',
+                r'.*SSL Library Error:.*:SSL routines::no shared cipher.*'
+            ]
+        )
index b7e4eaef6ede7c4e1575aecc6f9f8062285d6916..4b0cad56a248a7c970348046ebba5370a912bdaa 100644 (file)
@@ -39,3 +39,9 @@ class TestRequire:
         assert 0 == r.exit_code
         assert r.response
         assert 403 == r.response["status"]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01630"   # client denied by server configuration
+            ]
+        )
index 2fa7d1d68a2e2ee59484d01a925104a7cddeb79b..1542450df9371b02f1e3529027ed027f7fda7d2f 100644 (file)
@@ -90,6 +90,9 @@ class TestUpgrade:
         url = env.mkurl("http", "test1", "/index.html")
         r = env.nghttp().get(url, options=["-u"])
         assert r.response["status"] == 200
+        # check issue #272
+        assert 'date' in r.response["header"], f'{r.response}'
+        assert r.response["header"]["date"] != 'Sun, 00 Jan 1900 00:00:00 GMT', f'{r.response}'
 
     # upgrade to h2c for a request where http/1.1 is preferred, but the clients upgrade
     # wish is honored nevertheless
index f7d3859cafb412fd436569741a9715f7c5e79bf6..22160b45853dd429568a9043c54b6167d1f03181 100644 (file)
@@ -42,6 +42,13 @@ class TestTimeout:
         except Exception as ex:
             print(f"as expected: {ex}")
         sock.close()
+        #
+        time.sleep(1) # let the log flush
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10373"   # SSL handshake was not completed
+            ]
+        )
 
     # Check that mod_reqtimeout handshake setting takes effect
     def test_h2_105_02(self, env):
@@ -77,6 +84,13 @@ class TestTimeout:
         except Exception as ex:
             print(f"as expected: {ex}")
         sock.close()
+        #
+        time.sleep(1) # let the log flush
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10373"   # SSL handshake was not completed
+            ]
+        )
 
     # Check that mod_reqtimeout handshake setting do no longer apply to handshaked 
     # connections. See <https://github.com/icing/mod_h2/issues/196>.
index 83e143cef5811dea85a95d062c04e046000119d7..fab881bcac76cf22544aa10aac674b647e0a30ae 100644 (file)
@@ -72,4 +72,10 @@ class TestShutdown:
             else:
                 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
+                assert "HTTP/2" == r.response["protocol"]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH03490"   # scoreboard is full, not at MaxRequestWorkers
+            ]
+        )
\ No newline at end of file
index 5b3aafd8fab4753685d16dd289296261df4114f6..04c022c362dc88045c2ec04f7318454565c55b4f 100644 (file)
@@ -28,6 +28,15 @@ class TestInvalidHeaders:
                 assert 500 == r.response["status"], f'unexpected status for char 0x{x:02}'
             else:
                 assert 0 != r.exit_code, f'unexpected exit code for char 0x{x:02}'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02429"   # Response header name contains invalid characters
+            ],
+            matches = [
+                r'.*malformed header from script \'hecho.py\': Bad header: x.*'
+            ]
+        )
 
     # let the hecho.py CGI echo chars < 0x20 in field value
     # for almost all such characters, the stream returns a 500
@@ -46,6 +55,12 @@ class TestInvalidHeaders:
                     assert 500 == r.response["status"], f'unexpected status for char 0x{x:02}'
                 else:
                     assert 0 != r.exit_code, "unexpected exit code for char 0x%02x" % x
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02430"   # Response header value contains invalid characters
+            ]
+        )
 
     # let the hecho.py CGI echo 0x10 and 0x7f in field name and value
     def test_h2_200_03(self, env):
@@ -63,6 +78,13 @@ class TestInvalidHeaders:
                 assert 500 == r.response["status"], f"unexpected exit code for char 0x{h:02}"
             else:
                 assert 0 != r.exit_code
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH02429",  # Response header name contains invalid characters
+                "AH02430"   # Response header value contains invalid characters
+            ]
+        )
 
     # test header field lengths check, LimitRequestLine
     def test_h2_200_10(self, env):
index 9fc8f3b2242cd717748fdff76aa86bc9934a330d..1fe3e131595a95e285263a4174ce382fa5527f21 100644 (file)
@@ -1,4 +1,5 @@
 import pytest
+from typing import List, Optional
 
 from pyhttpd.env import HttpdTestEnv
 from .env import H2Conf
@@ -22,17 +23,17 @@ class TestRfc9113:
         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
+    @pytest.mark.parametrize(["hvalue", "expvalue", "status", "lognos"], [
+        ['"123"', '123', 200, None],
+        ['"123 "', '123', 200, None],       # trailing space stripped
+        ['"123\t"', '123', 200, None],     # trailing tab stripped
+        ['" 123"', '123', 200, None],        # leading space is stripped
+        ['"          123"', '123', 200, None],  # leading spaces are stripped
+        ['"\t123"', '123', 200, None],       # leading tab is stripped
+        ['"expr=%{unescape:123%0A 123}"', '', 500, ["AH02430"]],  # illegal char
+        ['" \t "', '', 200, None],          # just ws
     ])
-    def test_h2_203_02(self, env, hvalue, expvalue, status):
+    def test_h2_203_02(self, env, hvalue, expvalue, status, lognos: Optional[List[str]]):
         hname = 'ap-test-007'
         conf = H2Conf(env, extras={
             f'test1.{env.http_tld}': [
@@ -53,4 +54,7 @@ class TestRfc9113:
         assert r.response["status"] == status
         if int(status) < 400:
             assert r.response["header"][hname] == expvalue
+        #
+        if lognos is not None:
+            env.httpd_error_log.ignore_recent(lognos = lognos)
 
index 88a8ece3f6e98667b91a3da1ef95f080db6cf1bf..87e523c4a21f581ac36dc5770171b7b085645b63 100644 (file)
@@ -149,9 +149,21 @@ class TestProxy:
         url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout")
         r = env.curl_get(url)
         assert r.exit_code != 0, r
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01110"   # Network error reading response
+            ]
+        )
 
     # 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
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01110"   # Network error reading response
+            ]
+        )
index 18d5d1d4695773823c4412f5d01deafad6873d06..18a528e9c959aeef8c9a4c306847295123473822 100644 (file)
@@ -78,8 +78,8 @@ class TestH2Proxy:
         conf.install()
         assert env.apache_restart() == 0
         url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py")
-        # httpd 2.4.59 disables reuse, not matter the config
-        if enable_reuse == "on" and not env.httpd_is_at_least("2.4.59"):
+        # httpd 2.5.0 disables reuse, not matter the config
+        if enable_reuse == "on" and not env.httpd_is_at_least("2.4.60"):
             # reuse is not guaranteed for each request, but we expect some
             # to do it and run on a h2 stream id > 1
             reused = False
@@ -132,7 +132,7 @@ class TestH2Proxy:
         assert int(r.json[0]["port"]) == env.http_port
         assert r.response["status"] == 200
         exp_port = env.http_port if enable_reuse == "on" \
-                                    and not env.httpd_is_at_least("2.4.59")\
+                                    and not env.httpd_is_at_least("2.4.60")\
             else env.http_port2
         assert int(r.json[1]["port"]) == exp_port
 
@@ -188,7 +188,6 @@ class TestH2Proxy:
 
     # produce an error, fail to generate an error bucket
     def test_h2_600_32(self, env, repeat):
-        pytest.skip('only works reliable with r1911964 from trunk')
         conf = H2Conf(env)
         conf.add_vhost_cgi(h2proxy_self=True)
         conf.install()
index 78760fbf8cbf11f19390c89ea1c061b476249dbd..138e74ce8588e7f5b63f46c06a202c25cae01e66 100644 (file)
@@ -61,3 +61,37 @@ class TestLoadGet:
                 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 window sizes, connection and stream
+    @pytest.mark.parametrize("connbits,streambits", [
+        [10, 16],  # 1k connection window, 64k stream windows
+        [10, 30],  # 1k connection window, huge stream windows
+        [30, 8],  # huge conn window, 256 bytes stream windows
+    ])
+    @pytest.mark.skip('awaiting mpm_event improvements')
+    def test_h2_700_20(self, env, connbits, streambits):
+        if not env.httpd_is_at_least("2.5.0"):
+            pytest.skip(f'need at least httpd 2.5.0 for this')
+        conf = H2Conf(env, extras={
+            'base': [
+                'StartServers 1',
+            ]
+        })
+        conf.add_vhost_cgi().add_vhost_test1().install()
+        assert env.apache_restart() == 0
+        assert env.is_live()
+        n = 2000
+        conns = 50
+        parallel = 10
+        args = [
+            env.h2load,
+            '-n', f'{n}', '-t', '1',
+            '-c', f'{conns}', '-m', f'{parallel}',
+            '-W', f'{connbits}',  # connection window bits
+            '-w', f'{streambits}',  # stream window bits
+            f'--connect-to=localhost:{env.https_port}',
+            f'--base-uri={env.mkurl("https", "test1", "/")}',
+            "/data-100k"
+        ]
+        r = env.run(args)
+        self.check_h2load_ok(env, r, n)
\ No newline at end of file
index 6658441231e863657714c11980156e6307180ef8..0a6978b42773d835958e8ab28e06dac25ba34594 100644 (file)
@@ -33,7 +33,7 @@ class TestBuffering:
         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)
+        stutter = timedelta(seconds=0.2)
         piper = CurlPiper(env=env, url=url)
         piper.stutter_check(chunks, stutter)
 
@@ -43,6 +43,16 @@ class TestBuffering:
         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=1)  # need a bit more delay since we have the extra connection
+        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)
index 52af1a3ae139b83bf2fbf0bc1d0e1b8244f488a3..c0fc0c23dcb3ad593e170e7988139e3523d96683 100644 (file)
@@ -84,8 +84,8 @@ def ws_run(env: H2TestEnv, path, authority=None, do_input=None, inbytes=None,
 
 
 @pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
-@pytest.mark.skipif(condition=not H2TestEnv().httpd_is_at_least("2.4.58"),
-                    reason=f'need at least httpd 2.4.58 for this')
+@pytest.mark.skipif(condition=not H2TestEnv().httpd_is_at_least("2.4.60"),
+                    reason=f'need at least httpd 2.4.60 for this')
 @pytest.mark.skipif(condition=ws_version < ws_version_min,
                     reason=f'websockets is {ws_version}, need at least {ws_version_min}')
 class TestWebSockets:
@@ -154,7 +154,6 @@ class TestWebSockets:
         r, infos, frames = ws_run(env, path='/ws/echo/', scenario='fail-proto')
         assert r.exit_code == 0, f'{r}'
         assert infos == ['[1] :status: 501', '[1] EOF'], f'{r}'
-        env.httpd_error_log.ignore_recent()
 
     # a correct CONNECT, send CLOSE, expect CLOSE, basic success
     def test_h2_800_02_ws_empty(self, env: H2TestEnv, ws_server):
index 04165a2dfcd1f62f586578e4b99344bdcdaab500..0f9e4a9f490df87285feea6c0d3ec25313fac2bc 100755 (executable)
@@ -1,6 +1,5 @@
 import logging
 import os
-import re
 import sys
 import pytest
 
@@ -33,48 +32,18 @@ def env(pytestconfig) -> MDTestEnv:
     env.setup_httpd()
     env.apache_access_log_clear()
     env.httpd_error_log.clear_log()
-    return env
+    yield env
+    env.apache_stop()
 
 
 @pytest.fixture(autouse=True, scope="package")
-def _session_scope(env):
-    # we'd like to check the httpd error logs after the test suite has
-    # run to catch anything unusual. For this, we setup the ignore list
-    # of errors and warnings that we do expect.
-    env.httpd_error_log.set_ignored_lognos([
-        'AH10040',  # mod_md, setup complain
-        'AH10045',  # mod_md complains that there is no vhost for an MDomain
-        'AH10056',  # mod_md, invalid params
-        'AH10105',  # mod_md does not find a vhost with SSL enabled for an MDomain
-        'AH10085',  # mod_ssl complains about fallback certificates
-        'AH01909',  # mod_ssl, cert alt name complains
-        'AH10170',  # mod_md, wrong config, tested
-        'AH10171',  # mod_md, wrong config, tested
-        'AH10373',  # SSL errors on uncompleted handshakes
-        'AH10398',  # test on global store lock
+def _md_package_scope(env):
+    env.httpd_error_log.add_ignored_lognos([
+        "AH10085",   # There are no SSL certificates configured and no other module contributed any
+        "AH10045",   # No VirtualHost matches Managed Domain
+        "AH10105",   # MDomain does not match any VirtualHost with 'SSLEngine on'
     ])
 
-    env.httpd_error_log.add_ignored_patterns([
-        re.compile(r'.*urn:ietf:params:acme:error:.*'),
-        re.compile(r'.*None of the ACME challenge methods configured for this domain are suitable.*'),
-        re.compile(r'.*problem\[(challenge-mismatch|challenge-setup-failure|apache:eab-hmac-invalid)].*'),
-        re.compile(r'.*CA considers answer to challenge invalid.].*'),
-        re.compile(r'.*problem\[urn:org:apache:httpd:log:AH\d+:].*'),
-        re.compile(r'.*Unsuccessful in contacting ACME server at :*'),
-        re.compile(r'.*test-md-720-002-\S+.org: dns-01 setup command failed .*'),
-        re.compile(r'.*AH\d*: unable to obtain global registry lock, .*'),
-    ])
-    if env.lacks_ocsp():
-        env.httpd_error_log.add_ignored_patterns([
-            re.compile(r'.*certificate with serial \S+ has no OCSP responder URL.*'),
-        ])
-    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))
-
 
 @pytest.fixture(scope="package")
 def acme(env):
index 85371ba227b9a5635c2c12db58ff47ca74349a96..88df1683413973241e5a27675603966d4933d52d 100644 (file)
@@ -15,7 +15,8 @@ from .md_env import MDTestEnv
 class TestConf:
 
     @pytest.fixture(autouse=True, scope='class')
-    def _class_scope(self, env):
+    def _class_scope(self, env, acme):
+        acme.start(config='default')
         env.clear_store()
 
     # test case: just one MDomain definition
@@ -24,6 +25,12 @@ class TestConf:
             MDomain not-forbidden.org www.not-forbidden.org mail.not-forbidden.org
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: two MDomain definitions, non-overlapping
     def test_md_300_002(self, env):
@@ -32,6 +39,12 @@ class TestConf:
             MDomain example2.org www.example2.org mail.example2.org
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: two MDomain definitions, exactly the same
     def test_md_300_003(self, env):
@@ -41,6 +54,12 @@ class TestConf:
             MDomain not-forbidden.org www.not-forbidden.org mail.not-forbidden.org test3.not-forbidden.org
             """).install()
         assert env.apache_fail() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10038"   # two Managed Domains have an overlap in domain
+            ]
+        )
 
     # test case: two MDomain definitions, overlapping
     def test_md_300_004(self, env):
@@ -50,6 +69,12 @@ class TestConf:
             MDomain example2.org test3.not-forbidden.org www.example2.org mail.example2.org
             """).install()
         assert env.apache_fail() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10038"   # two Managed Domains have an overlap in domain
+            ]
+        )
 
     # test case: two MDomains, one inside a virtual host
     def test_md_300_005(self, env):
@@ -60,6 +85,12 @@ class TestConf:
             </VirtualHost>
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: two MDomains, one correct vhost name
     def test_md_300_006(self, env):
@@ -71,6 +102,12 @@ class TestConf:
             </VirtualHost>
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: two MDomains, two correct vhost names
     def test_md_300_007(self, env):
@@ -85,6 +122,12 @@ class TestConf:
             </VirtualHost>
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: two MDomains, overlapping vhosts
     def test_md_300_008(self, env):
@@ -102,6 +145,12 @@ class TestConf:
             </VirtualHost>
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: vhosts with overlapping MDs
     def test_md_300_009(self, env):
@@ -118,7 +167,12 @@ class TestConf:
         conf.install()
         assert env.apache_fail() == 0
         env.apache_stop()
-        env.httpd_error_log.ignore_recent()
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10238"   # 2 MDs match Virtualhost
+            ]
+        )
 
     # test case: MDomain, vhost with matching ServerAlias
     def test_md_300_010(self, env):
@@ -146,6 +200,9 @@ class TestConf:
         conf.install()
         assert env.apache_fail() == 0
         env.apache_stop()
+        env.httpd_error_log.ignore_recent([
+            "AH10040"   # A requested MD certificate will not match ServerName
+        ])
 
     # test case: MDomain, misses one ServerAlias, but auto add enabled
     def test_md_300_011b(self, env):
@@ -171,6 +228,12 @@ class TestConf:
             </VirtualHost>
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain
+            ]
+        )
 
     # test case: one md covers two vhosts
     def test_md_300_013(self, env):
@@ -261,7 +324,6 @@ class TestConf:
         MDConf(env, text=line).install()
         assert env.apache_fail() == 0, "Server accepted test config {}".format(line)
         assert exp_err_msg in env.apachectl_stderr
-        env.httpd_error_log.ignore_recent()
 
     # test case: alt-names incomplete detection, github isse #68
     def test_md_300_021(self, env):
@@ -294,6 +356,12 @@ class TestConf:
             </VirtualHost>
             """).install()
         assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10105"   # MD secret.com does not match any VirtualHost with 'SSLEngine on'
+            ]
+        )
 
     # test case: use MDRequireHttps not in <Directory
     def test_md_300_023(self, env):
@@ -346,7 +414,7 @@ class TestConf:
     def test_md_300_026(self, env):
         assert env.apache_stop() == 0
         conf = MDConf(env)
-        domain = f"t300_026.{env.http_tld}"
+        domain = f"t300-026.{env.http_tld}"
         conf.add(f"""
             MDomain {domain}
             """)
@@ -388,3 +456,92 @@ class TestConf:
             assert len(md['ca']['urls']) == len(cas)
         else:
             assert rv != 0, "Server should not have accepted CAs '{}'".format(cas)
+
+    # messy ServerAliases, see #301
+    def test_md_300_028(self, env):
+        assert env.apache_stop() == 0
+        conf = MDConf(env)
+        domaina = f"t300-028a.{env.http_tld}"
+        domainb = f"t300-028b.{env.http_tld}"
+        dalias = f"t300-028alias.{env.http_tld}"
+        conf.add_vhost(port=env.http_port, domains=[domaina, domainb, dalias], with_ssl=False)
+        conf.add(f"""
+            MDMembers manual
+            MDomain {domaina} 
+            MDomain {domainb} {dalias}
+            """)
+        conf.add(f"""
+            <VirtualHost 10.0.0.1:{env.https_port}>
+              ServerName {domaina}
+              ServerAlias {dalias}
+              SSLEngine on
+            </VirtualHost>
+            <VirtualHost 10.0.0.1:{env.https_port}>
+              ServerName {domainb}
+              ServerAlias {dalias}
+              SSLEngine on
+            </VirtualHost>
+            """)
+        conf.install()
+        # This does not work as we have both MDs match domain's vhost
+        assert env.apache_fail() == 0
+        env.httpd_error_log.ignore_recent(
+            lognos=[
+                "AH10238",   # 2 MDs match the same vhost
+            ]
+        )
+        # It works, if we only match on ServerNames
+        conf.add("MDMatchNames servernames")
+        conf.install()
+        assert env.apache_restart() == 0
+        env.httpd_error_log.ignore_recent(
+            lognos=[
+                "AH10040",  # ServerAlias not covered
+            ]
+        )
+
+    # wildcard and specfic MD overlaps
+    def test_md_300_029(self, env):
+        assert env.apache_stop() == 0
+        conf = MDConf(env)
+        domain = f"t300-029.{env.http_tld}"
+        subdomain = f"sub.{domain}"
+        conf.add_vhost(port=env.http_port, domains=[domain, subdomain], with_ssl=False)
+        conf.add(f"""
+            MDMembers manual
+            MDomain {domain} *.{domain} 
+            MDomain {subdomain}
+            """)
+        conf.add(f"""
+            <VirtualHost 10.0.0.1:{env.https_port}>
+              ServerName {domain}
+              SSLEngine on
+            </VirtualHost>
+            <VirtualHost 10.0.0.1:{env.https_port}>
+              ServerName another.{domain}
+              SSLEngine on
+            </VirtualHost>
+            <VirtualHost 10.0.0.1:{env.https_port}>
+              ServerName {subdomain}
+              SSLEngine on
+            </VirtualHost>
+            """)
+        conf.install()
+        # This does not work as we have overlapping names in MDs
+        assert env.apache_fail() == 0
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10038"   # 2 MDs overlap
+            ]
+        )
+        # It works, if we only match on ServerNames
+        conf.add("MDMatchNames servernames")
+        conf.install()
+        assert env.apache_restart() == 0
+        time.sleep(2)
+        assert env.apache_stop() == 0
+        # we need dns-01 challenge for the wildcard, which is not configured
+        env.httpd_error_log.ignore_recent(matches=[
+            r'.*None of offered challenge types.*are supported.*'
+        ])
+
index 8e8f5f155c60ae7b1647605160da2cfb1926a4f8..04a9c7561aa95465c842a3a712ae30945cc7d0c7 100644 (file)
@@ -64,6 +64,12 @@ class TestAutov2:
         # file system needs to have correct permissions
         env.check_dir_empty(env.store_challenges())
         env.check_file_permissions(domain)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10045"   # No VirtualHost matches Managed Domain test-md-702-001-1688648129.org
+            ]
+        )
 
     # test case: same as test_702_001, but with two parallel managed domains
     def test_md_702_002(self, env):
@@ -234,6 +240,15 @@ class TestAutov2:
         cert = env.get_cert(name_a)
         assert name_a in cert.get_san_list()
         assert env.get_http_status(name_a, "/name.txt") == 503
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of offered challenge types
+            ],
+            matches = [
+                r'.*problem\[challenge-mismatch\].*'
+            ]
+        )
 
     # Specify a non-working http proxy
     def test_md_702_008(self, env):
@@ -254,6 +269,15 @@ class TestAutov2:
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['status-description'] == 'Connection refused'
         assert 'account' not in md['ca']
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # Unsuccessful in contacting ACME server
+            ],
+            matches = [
+                r'.*Unsuccessful in contacting ACME server at .*'
+            ]
+        )
 
     # Specify a valid http proxy
     def test_md_702_008a(self, env):
@@ -335,6 +359,16 @@ class TestAutov2:
         assert env.apache_restart() == 0
         env.check_md(domains)
         assert env.await_completion([domain])
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10173",  # None of the ACME challenge methods configured for this domain are suitable
+                "AH10056"   # None of the ACME challenge methods configured for this domain are suitable
+            ],
+            matches = [
+                r'.*None of the ACME challenge methods configured for this domain are suitable.*'
+            ]
+        )
 
     def test_md_702_011(self, env):
         domain = self.test_domain
@@ -364,6 +398,16 @@ class TestAutov2:
         assert env.apache_restart() == 0
         env.check_md(domains)
         assert env.await_completion([domain])
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10173",  # None of the ACME challenge methods configured for this domain are suitable
+                "AH10056"   # None of the ACME challenge methods configured for this domain are suitable
+            ],
+            matches = [
+                r'.*None of the ACME challenge methods configured for this domain are suitable.*'
+            ]
+        )
 
     # test case: one MD with several dns names. sign up. remove the *first* name
     # in the MD. restart. should find and keep the existing MD.
@@ -648,6 +692,16 @@ class TestAutov2:
         conf.install()
         assert env.apache_restart() == 0
         assert env.await_error(domain)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10173",  # None of the ACME challenge methods configured for this domain are suitable
+                "AH10056"   # None of the ACME challenge methods configured for this domain are suitable
+            ],
+            matches = [
+                r'.*None of the ACME challenge methods configured for this domain are suitable.*'
+            ]
+        )
 
     # Make a setup using the base server without http:, but with acme-tls/1, should work.
     def test_md_702_052(self, env):
index 23b311c3a4719385ab6e427616809f6ad9d0c239..916c47a5d852801994dd6e1a92e9c28933058431 100644 (file)
@@ -44,6 +44,15 @@ class TestWildcard:
         assert md
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'challenge-mismatch'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of offered challenge types
+            ],
+            matches = [
+                r'.*problem\[challenge-mismatch\].*'
+            ]
+        )
 
     # test case: a wildcard certificate with ACMEv2, only dns-01 configured, invalid command path
     def test_md_720_002(self, env):
@@ -67,6 +76,16 @@ class TestWildcard:
         assert md
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'challenge-setup-failure'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of offered challenge types
+            ],
+            matches = [
+                r'.*problem\[challenge-setup-failure\].*',
+                r'.*setup command failed to execute.*'
+            ]
+        )
 
     # variation, invalid cmd path, other challenges still get certificate for non-wildcard
     def test_md_720_002b(self, env):
@@ -113,6 +132,15 @@ class TestWildcard:
         assert md
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'challenge-setup-failure'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of offered challenge types
+            ],
+            matches = [
+                r'.*problem\[challenge-setup-failure\].*'
+            ]
+        )
 
     # test case: a wildcard name certificate with ACMEv2, only dns-01 configured
     def test_md_720_004(self, env):
index f7f7b4b2cfe5f6f19386ce669780320a304fc4c9..891ae620bb8e2d394add2bfd8d26a6d68a463a12 100644 (file)
@@ -115,3 +115,10 @@ class TestStatic:
         conf.add_vhost(domain)
         conf.install()
         assert env.apache_fail() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10170",  # Managed Domain needs one MDCertificateKeyFile for each MDCertificateFile
+                "AH10171"   # Managed Domain has MDCertificateKeyFile(s) but no MDCertificateFile
+            ]
+        )
index 670c9ab831a2bbbf94a47a2a0737af8e4a709753..364aaca6c8dffb7786555b3d27d3706dd22e1309 100644 (file)
@@ -46,6 +46,15 @@ class TestAcmeErrors:
             assert md['renewal']['last']['detail'] == (
                     "Error creating new order :: Cannot issue for "
                     "\"%s\": Domain name contains an invalid character" % domains[1])
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # Order included DNS identifier with a value containing an illegal character
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:malformed.*'
+            ]
+        )
 
     # test case: MD with 3 names, 2 invalid
     #
@@ -70,3 +79,12 @@ class TestAcmeErrors:
                 "Error creating new order :: Cannot issue for")
             assert md['renewal']['last']['subproblems']
             assert len(md['renewal']['last']['subproblems']) == 2
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # Order included DNS identifier with a value containing an illegal character
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:malformed.*'
+            ]
+        )
index 49b4e788c06559c0cae74d0cbc80a5167b7f6293..9ad79f0b1e98db31eca6d8a1bde4442365482e0e 100644 (file)
@@ -46,3 +46,13 @@ class TestSetupErrors:
         md = env.await_error(domain, errors=2, timeout=10)
         assert md
         assert md['renewal']['errors'] > 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # CA considers answer to challenge invalid
+            ],
+            matches = [
+                r'.*The key authorization file from the server did not match this challenge.*',
+                r'.*CA considers answer to challenge invalid.*'
+            ]
+        )
index af1be95d05a0a777c6727ae16c2ebb2ca55533c1..aec7e89b8c2542fe7fa6dac0bc8d14f732c595cf 100644 (file)
@@ -37,6 +37,15 @@ class TestEab:
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
+            ]
+        )
 
     def test_md_750_002(self, env):
         # md with known EAB KID and non base64 hmac key configured
@@ -51,6 +60,15 @@ class TestEab:
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'apache:eab-hmac-invalid'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # external account binding HMAC value is not valid base64
+            ],
+            matches = [
+                r'.*problem\[apache:eab-hmac-invalid\].*'
+            ]
+        )
 
     def test_md_750_003(self, env):
         # md with empty EAB KID configured
@@ -64,7 +82,19 @@ class TestEab:
         assert env.apache_restart() == 0
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
-        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+        assert md['renewal']['last']['problem'] in [
+            'urn:ietf:params:acme:error:unauthorized',
+            'urn:ietf:params:acme:error:malformed',
+        ]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # the field 'kid' references a key that is not known to the ACME server
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:(unauthorized|malformed).*'
+            ]
+        )
 
     def test_md_750_004(self, env):
         # md with unknown EAB KID configured
@@ -78,7 +108,19 @@ class TestEab:
         assert env.apache_restart() == 0
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
-        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+        assert md['renewal']['last']['problem'] in [
+            'urn:ietf:params:acme:error:unauthorized',
+            'urn:ietf:params:acme:error:malformed',
+        ]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # the field 'kid' references a key that is not known to the ACME server
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:(unauthorized|malformed).*'
+            ]
+        )
 
     def test_md_750_005(self, env):
         # md with known EAB KID but wrong HMAC configured
@@ -92,7 +134,19 @@ class TestEab:
         assert env.apache_restart() == 0
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
-        assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+        assert md['renewal']['last']['problem'] in [
+            'urn:ietf:params:acme:error:unauthorized',
+            'urn:ietf:params:acme:error:malformed',
+        ]
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # external account binding JWS verification error: square/go-jose: error in cryptographic primitive
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:(unauthorized|malformed).*'
+            ]
+        )
 
     def test_md_750_010(self, env):
         # md with correct EAB configured
@@ -125,6 +179,15 @@ class TestEab:
         md = env.await_error(domain_b)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
+            ]
+        )
 
     def test_md_750_012(self, env):
         # first one md without EAB, then one with
@@ -144,6 +207,15 @@ class TestEab:
         md = env.await_error(domain_a)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
+            ]
+        )
 
     def test_md_750_013(self, env):
         # 2 mds with the same EAB, should one create a single account
@@ -215,6 +287,15 @@ class TestEab:
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
+            ]
+        )
 
     def test_md_750_016(self, env):
         # md with correct EAB, get cert, change to invalid EAB
@@ -241,6 +322,15 @@ class TestEab:
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:unauthorized'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # the field 'kid' references a key that is not known to the ACME server
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:unauthorized.*'
+            ]
+        )
 
     def test_md_750_017(self, env):
         # md without EAB explicitly set to none
@@ -257,6 +347,15 @@ class TestEab:
         md = env.await_error(domain)
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['problem'] == 'urn:ietf:params:acme:error:externalAccountRequired'
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # ACME server policy requires newAccount requests must include a value for the 'externalAccountBinding' field
+            ],
+            matches = [
+                r'.*urn:ietf:params:acme:error:externalAccountRequired.*'
+            ]
+        )
 
     def test_md_750_018(self, env):
         # md with EAB file that does not exist
index 84a266b2eb783df8c53cdd87d6e06b8910dbbdb6..27a2df474aa636254b2b3f9de8b8952e6438554f 100644 (file)
@@ -140,6 +140,12 @@ class TestTailscale:
         assert md['renewal']['last']['status-description'] == 'No such file or directory'
         assert md['renewal']['last']['detail'] == \
                f"tailscale socket not available, may not be up: {socket_path}"
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # retrieving certificate from tailscale
+            ]
+        )
 
     # create a MD using `tailscale` as protocol, path to faker, should succeed
     def test_md_780_002(self, env):
@@ -184,3 +190,9 @@ class TestTailscale:
         assert md['renewal']['errors'] > 0
         assert md['renewal']['last']['status-description'] == 'No such file or directory'
         assert md['renewal']['last']['detail'] == "retrieving certificate from tailscale"
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # retrieving certificate from tailscale
+            ]
+        )
index a93991233d386981d174a54a4bbb371cbd308e1a..696161fd4fd8d8050688c775deba01d905bf8431 100644 (file)
@@ -63,6 +63,15 @@ class TestFailover:
         assert env.apache_restart() == 0
         assert env.await_completion([domain])
         env.check_md_complete(domain)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # Unsuccessful in contacting ACME server
+            ],
+            matches = [
+                r'.*Unsuccessful in contacting ACME server at .*'
+            ]
+        )
 
     # set 3 ACME certificata authority, invalid + invalid + valid
     def test_md_790_003(self, env):
@@ -85,3 +94,12 @@ class TestFailover:
         assert env.apache_restart() == 0
         assert env.await_completion([domain])
         env.check_md_complete(domain)
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # Unsuccessful in contacting ACME server
+            ],
+            matches = [
+                r'.*Unsuccessful in contacting ACME server at .*'
+            ]
+        )
index 30e0742036ce6040a7d035d1b78d94327777cbe1..9d18da5411491911ee72a02c8b071d6a08ca2af0 100644 (file)
@@ -49,6 +49,12 @@ class TestNotify:
         assert env.await_error(self.domain)
         stat = env.get_md_status(self.domain)
         assert stat["renewal"]["last"]["problem"] == "urn:org:apache:httpd:log:AH10108:"
+        #
+        env.httpd_error_log.ignore_recent(
+            matches = [
+                r'.*urn:org:apache:httpd:log:AH10108:.*'
+            ]
+        )
 
     # test: valid notify cmd that fails, check error
     def test_md_900_002(self, env):
@@ -61,6 +67,14 @@ class TestNotify:
         assert env.await_error(self.domain)
         stat = env.get_md_status(self.domain)
         assert stat["renewal"]["last"]["problem"] == "urn:org:apache:httpd:log:AH10108:"
+        #
+        env.httpd_error_log.ignore_recent(
+            matches = [
+                r'.*urn:org:apache:httpd:log:AH10108:.*',
+                r'.*urn:org:apache:httpd:log:AH10109:.*'
+                r'.*problem\[challenge-setup-failure\].*',
+            ]
+        )
 
     # test: valid notify that logs to file
     def test_md_900_010(self, env):
index 8d03bfd6a31ba818bf10f4717c00a7c16b863c64..b18cfd38d447b217560731e05f819b11578a2b3b 100644 (file)
@@ -46,6 +46,16 @@ class TestMessage:
         stat = env.get_md_status(domain)
         # this command should have failed and logged an error
         assert stat["renewal"]["last"]["problem"] == "urn:org:apache:httpd:log:AH10109:"
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of the offered challenge types
+            ],
+            matches = [
+                r'.*urn:org:apache:httpd:log:AH10109:.*',
+                r'.*problem\[challenge-setup-failure\].*'
+            ]
+        )
 
     # test: signup with configured message cmd that is valid but returns != 0
     def test_md_901_002(self, env):
@@ -63,6 +73,16 @@ class TestMessage:
         stat = env.get_md_status(domain)
         # this command should have failed and logged an error
         assert stat["renewal"]["last"]["problem"] == "urn:org:apache:httpd:log:AH10109:"
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of the offered challenge types
+            ],
+            matches = [
+                r'.*urn:org:apache:httpd:log:AH10109:.*',
+                r'.*problem\[challenge-setup-failure\].*'
+            ]
+        )
 
     # test: signup with working message cmd and see that it logs the right things
     def test_md_901_003(self, env):
@@ -247,7 +267,6 @@ class TestMessage:
                     assert job["last"]["problem"] == "urn:org:apache:httpd:log:AH10109:"
                     break
             time.sleep(0.1)
-        env.httpd_error_log.ignore_recent()
 
         # reconfigure to a working notification command and restart
         conf = MDConf(env)
@@ -294,4 +313,13 @@ class TestMessage:
         stat = env.get_md_status(domain)
         # this command should have failed and logged an error
         assert stat["renewal"]["last"]["problem"] == "challenge-setup-failure"
-
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10056"   # None of the offered challenge types
+            ],
+            matches = [
+                r'.*urn:org:apache:httpd:log:AH10109:.*',
+                r'.*problem\[challenge-setup-failure\].*'
+            ]
+        )
index c89ce6d8d7087a8febd68fd6fdbe709bc32989f2..6ad708728c771bda1b21f483b7f8294c5a8ff49b 100644 (file)
@@ -243,3 +243,9 @@ Protocols h2 http/1.1 acme-tls/1
             assert ktype in stat['cert']
             if env.acme_server == 'boulder':
                 assert 'ocsp' in stat['cert'][ktype]
+        #
+        env.httpd_error_log.ignore_recent(
+            matches = [
+                r'.*certificate with serial \w+ has no OCSP responder URL.*'
+            ]
+        )
index 23c5f14201f85613050a45ffe2ad0c696a36a7ca..7e6f4e7b09d2f34f026d8ef5093a05e930c50db8 100644 (file)
@@ -29,23 +29,3 @@ def env(pytestconfig) -> ProxyTestEnv:
     env.apache_access_log_clear()
     env.httpd_error_log.clear_log()
     return env
-
-
-@pytest.fixture(autouse=True, scope="package")
-def _session_scope(env):
-    # we'd like to check the httpd error logs after the test suite has
-    # run to catch anything unusual. For this, we setup the ignore list
-    # of errors and warnings that we do expect.
-    env.httpd_error_log.set_ignored_lognos([
-        'AH01144',  # No protocol handler was valid for the URL
-    ])
-
-    env.httpd_error_log.add_ignored_patterns([
-        #re.compile(r'.*urn:ietf:params:acme:error:.*'),
-    ])
-    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))
index 9ed635cd5fbb2d8b48163d623b27b7a74bfd5884..098d4d4948519495c4d1df020360597866fff528 100644 (file)
@@ -1,7 +1,6 @@
 import inspect
 import logging
 import os
-import re
 import subprocess
 from typing import Dict, Any
 
index 7f3d4d55b2b34ce8c098d1b905321aff9be81eea..0c39bc9c12c4af1247962078e35b99339ff648f4 100644 (file)
@@ -153,6 +153,12 @@ Host: {domain}
         r2 = self.parse_response(rlines)
         assert r2.response
         assert r2.response['status'] == exp_status
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01144"   #  No protocol handler was valid for the URL
+            ]
+        )
 
     def parse_response(self, lines) -> ExecResult:
         exp_body = False
index ddeb91f99a894c40c79b92e2c823bc05b63c6dc9..b34f746004cf04f6e0a43ebfafc3b230d9745a89 100644 (file)
@@ -13,7 +13,10 @@ class TlsTestConf(HttpdConf):
 
     def start_tls_vhost(self, domains: List[str], port=None, ssl_module=None):
         if ssl_module is None:
-            ssl_module = 'mod_tls'
+            if not self.env.has_shared_module("tls"):
+                ssl_module = "mod_ssl"
+            else:
+                ssl_module = 'mod_tls'
         super().start_vhost(domains=domains, port=port, doc_root=f"htdocs/{domains[0]}", ssl_module=ssl_module)
 
     def end_tls_vhost(self):
@@ -39,8 +42,12 @@ class TlsTestConf(HttpdConf):
                     f"    MDCertificateKeyFile {pkey_file}",
                     ])
             self.add("</MDomain>")
+            if self.env.has_shared_module("tls"):
+                ssl_module= "mod_tls"
+            else:
+                ssl_module= "mod_ssl"
             super().add_vhost(domains=[domain], port=port, doc_root=f"htdocs/{domain}",
-                              with_ssl=True, with_certificates=False, ssl_module='mod_tls')
+                              with_ssl=True, with_certificates=False, ssl_module=ssl_module)
 
     def add_md_base(self, domain: str):
         self.add([
index cde4be607140e5d99f2af5506a2cfac5f52266a3..c7cb85877d533c700bb9607cfb5e0efa1e043b91 100644 (file)
@@ -31,9 +31,3 @@ def env(pytestconfig) -> TlsTestEnv:
     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
index 0e457bf13763e7765e1181e52551410b8f987b5f..6afc472cb082fe7047f892b76929d10c6f6f1fad 100644 (file)
@@ -129,7 +129,10 @@ class TlsTestEnv(HttpdTestEnv):
             ]),
             CertificateSpec(name="user1", client=True, single_file=True),
         ])
-        self.add_httpd_log_modules(['tls'])
+        if not HttpdTestEnv.has_shared_module("tls"):
+            self.add_httpd_log_modules(['ssl'])
+        else:
+            self.add_httpd_log_modules(['tls'])
 
 
     def setup_httpd(self, setup: TlsTestSetup = None):
index 4d6aa60200a3f302668db04f8385ed5b36019640..88be80c3a673194a11eee311810a606fcd1c3826 100644 (file)
@@ -64,9 +64,15 @@ class TestConf:
     ])
     def test_tls_02_conf_cert_listen_valid(self, env, listen: str):
         conf = TlsTestConf(env=env)
-        conf.add("TLSEngine {listen}".format(listen=listen))
-        conf.install()
-        assert env.apache_restart() == 0
+        if not env.has_shared_module("tls"):
+            # Without cert/key openssl will complain
+            conf.add("SSLEngine on");
+            conf.install()
+            assert env.apache_restart() == 1
+        else:
+            conf.add("TLSEngine {listen}".format(listen=listen))
+            conf.install()
+            assert env.apache_restart() == 0
 
     def test_tls_02_conf_cert_listen_cert(self, env):
         domain = env.domain_a
index cf421c0fe815d6f187404a58d9ef19f6a96e5f00..cbd142afbc980eea3dc9d8abd9f9e5d987222f95 100644 (file)
@@ -34,6 +34,12 @@ class TestSni:
         domain_unknown = "unknown.test"
         r = env.tls_get(domain_unknown, "/index.json")
         assert r.exit_code != 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10353"   # cannot decrypt peer's message
+            ]
+        )
 
     def test_tls_03_sni_request_other_same_config(self, env):
         # do we see the first vhost response for another domain with different certs?
@@ -44,6 +50,12 @@ class TestSni:
         assert r.exit_code == 0
         assert r.json is None
         assert r.response['status'] == 421
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10345"   # Connection host selected via SNI and request have incompatible TLS configurations
+            ]
+        )
 
     def test_tls_03_sni_request_other_other_honor(self, env):
         # do we see the first vhost response for an unknown domain?
@@ -60,6 +72,12 @@ class TestSni:
         # request denied
         assert r.exit_code == 0
         assert r.json is None
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10345"   # Connection host selected via SNI and request have incompatible TLS configurations
+            ]
+        )
 
     @pytest.mark.skip('openssl behaviour changed on ventura, unreliable')
     def test_tls_03_sni_bad_hostname(self, env):
index 2e60bdd75632201375d313d6f0d52dc93dac4233..4bedd692ceb4571716fea545bb85be67186bc563 100644 (file)
@@ -176,16 +176,21 @@ class TestCiphers:
 
     def test_tls_06_ciphers_pref_unsupported(self, env):
         # a warning on preferring a known, but not supported cipher
-        env.httpd_error_log.ignore_recent()
         conf = TlsTestConf(env=env, extras={
             env.domain_b: "TLSCiphersPrefer TLS_NULL_WITH_NULL_NULL"
         })
         conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
         conf.install()
-        assert env.apache_restart() == 0
-        (errors, warnings) = env.httpd_error_log.get_recent_count()
-        assert errors == 0
-        assert warnings == 2  # once on dry run, once on start
+        if not conf.env.has_shared_module("tls"):
+            assert env.apache_restart() != 0
+        else:
+            assert env.apache_restart() == 0
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH10319"   # Server has TLSCiphersPrefer configured that are not supported by rustls
+            ]
+        )
 
     def test_tls_06_ciphers_supp_unknown(self, env):
         conf = TlsTestConf(env=env, extras={
@@ -197,13 +202,11 @@ class TestCiphers:
 
     def test_tls_06_ciphers_supp_unsupported(self, env):
         # no warnings on suppressing known, but not supported ciphers
-        env.httpd_error_log.ignore_recent()
         conf = TlsTestConf(env=env, extras={
             env.domain_b: "TLSCiphersSuppress TLS_NULL_WITH_NULL_NULL"
         })
         conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
         conf.install()
+        if not conf.env.has_shared_module("tls"):
+            return
         assert env.apache_restart() == 0
-        (errors, warnings) = env.httpd_error_log.get_recent_count()
-        assert errors == 0
-        assert warnings == 0
index a8df99af2aafbe4409e80ffdd7b6b3e791b986bc..0e3ee74d2df910d1a04022d1aab96a84f22b6c6f 100644 (file)
@@ -23,7 +23,10 @@ class TestVars:
     def test_tls_08_vars_root(self, env):
         # in domain_b root, the StdEnvVars is switch on
         exp_proto = "TLSv1.2"
-        exp_cipher = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+        if env.has_shared_module("tls"):
+            exp_cipher = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
+        else:
+            exp_cipher = "ECDHE-ECDSA-AES256-GCM-SHA384"
         options = [ '--tls-max', '1.2']
         r = env.tls_get(env.domain_b, "/vars.py", options=options)
         assert r.exit_code == 0, r.stderr
@@ -47,7 +50,12 @@ class TestVars:
     def test_tls_08_vars_const(self, env, name: str, value: str):
         r = env.tls_get(env.domain_b, f"/vars.py?name={name}")
         assert r.exit_code == 0, r.stderr
-        assert r.json == {name: value}, r.stdout
+        if env.has_shared_module("tls"):
+            assert r.json == {name: value}, r.stdout
+        else:
+            if name == "SSL_SECURE_RENEG":
+                value = "true"
+            assert r.json == {name: value}, r.stdout
 
     @pytest.mark.parametrize("name, pattern", [
         ("SSL_VERSION_INTERFACE", r'mod_tls/\d+\.\d+\.\d+'),
@@ -57,4 +65,11 @@ class TestVars:
         r = env.tls_get(env.domain_b, f"/vars.py?name={name}")
         assert r.exit_code == 0, r.stderr
         assert name in r.json
-        assert re.match(pattern, r.json[name]), r.json
+        if env.has_shared_module("tls"):
+            assert re.match(pattern, r.json[name]), r.json
+        else:
+            if name == "SSL_VERSION_INTERFACE":
+                pattern = r'mod_ssl/\d+\.\d+\.\d+'
+            else:
+                pattern = r'OpenSSL/\d+\.\d+\.\d+'
+            assert re.match(pattern, r.json[name]), r.json
index cefcbf60011acb186ebe1aaccd9609a641e7036f..87e04c28afa48033a974e922310d2886d2513818 100644 (file)
@@ -2,6 +2,7 @@ import re
 import pytest
 
 from .conf import TlsTestConf
+from pyhttpd.env import HttpdTestEnv
 
 
 class TestProxySSL:
@@ -9,6 +10,12 @@ class TestProxySSL:
     @pytest.fixture(autouse=True, scope='class')
     def _class_scope(self, env):
         # add vhosts a+b and a ssl proxy from a to b
+        if not HttpdTestEnv.has_shared_module("tls"):
+            myoptions="SSLOptions +StdEnvVars"
+            myssl="mod_ssl"
+        else:
+            myoptions="TLSOptions +StdEnvVars"
+            myssl="mod_tls"
         conf = TlsTestConf(env=env, extras={
             'base': [
                 "LogLevel proxy:trace1 proxy_http:trace1 ssl:trace1 proxy_http2:trace1",
@@ -33,10 +40,10 @@ class TestProxySSL:
                 f'ProxyPass /proxy-ssl/ https://127.0.0.1:{env.https_port}/',
                 f'ProxyPass /proxy-local/ https://localhost:{env.https_port}/',
                 f'ProxyPass /proxy-h2-ssl/ h2://127.0.0.1:{env.https_port}/',
-                "TLSOptions +StdEnvVars",
+                myoptions,
             ],
         })
-        conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b])
+        conf.add_tls_vhosts(domains=[env.domain_a, env.domain_b], ssl_module=myssl)
         conf.install()
         assert env.apache_restart() == 0
 
@@ -48,6 +55,13 @@ class TestProxySSL:
         # does not work, since SSLProxy* not configured
         data = env.tls_get_json(env.domain_b, "/proxy-local/index.json")
         assert data is None
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01961",  # failed to enable ssl support [Hint: if using mod_ssl, see SSLProxyEngine]
+                "AH00961"   # failed to enable ssl support (mod_proxy)
+            ]
+        )
 
     def test_tls_14_proxy_ssl_h2_get(self, env):
         r = env.tls_get(env.domain_b, "/proxy-h2-ssl/index.json")
@@ -62,7 +76,24 @@ class TestProxySSL:
         ("SSL_CIPHER_EXPORT", "false"),
         ("SSL_CLIENT_VERIFY", "NONE"),
     ])
+    def test_tls_14_proxy_tsl_vars_const(self, env, name: str, value: str):
+        if not HttpdTestEnv.has_shared_module("tls"):
+            return
+        r = env.tls_get(env.domain_b, f"/proxy-ssl/vars.py?name={name}")
+        assert r.exit_code == 0, r.stderr
+        assert r.json == {name: value}, r.stdout
+
+    @pytest.mark.parametrize("name, value", [
+        ("SERVER_NAME", "b.mod-tls.test"),
+        ("SSL_SESSION_RESUMED", "Initial"),
+        ("SSL_SECURE_RENEG", "true"),
+        ("SSL_COMPRESS_METHOD", "NULL"),
+        ("SSL_CIPHER_EXPORT", "false"),
+        ("SSL_CLIENT_VERIFY", "NONE"),
+    ])
     def test_tls_14_proxy_ssl_vars_const(self, env, name: str, value: str):
+        if HttpdTestEnv.has_shared_module("tls"):
+            return
         r = env.tls_get(env.domain_b, f"/proxy-ssl/vars.py?name={name}")
         assert r.exit_code == 0, r.stderr
         assert r.json == {name: value}, r.stdout
@@ -71,7 +102,21 @@ class TestProxySSL:
         ("SSL_VERSION_INTERFACE", r'mod_tls/\d+\.\d+\.\d+'),
         ("SSL_VERSION_LIBRARY", r'rustls-ffi/\d+\.\d+\.\d+/rustls/\d+\.\d+(\.\d+)?'),
     ])
+    def test_tls_14_proxy_tsl_vars_match(self, env, name: str, pattern: str):
+        if not HttpdTestEnv.has_shared_module("tls"):
+            return
+        r = env.tls_get(env.domain_b, f"/proxy-ssl/vars.py?name={name}")
+        assert r.exit_code == 0, r.stderr
+        assert name in r.json
+        assert re.match(pattern, r.json[name]), r.json
+
+    @pytest.mark.parametrize("name, pattern", [
+        ("SSL_VERSION_INTERFACE", r'mod_ssl/\d+\.\d+\.\d+'),
+        ("SSL_VERSION_LIBRARY", r'OpenSSL/\d+\.\d+\.\d+'),
+    ])
     def test_tls_14_proxy_ssl_vars_match(self, env, name: str, pattern: str):
+        if HttpdTestEnv.has_shared_module("tls"):
+            return
         r = env.tls_get(env.domain_b, f"/proxy-ssl/vars.py?name={name}")
         assert r.exit_code == 0, r.stderr
         assert name in r.json
index f2f670d7e99d8d0997c847a3c32bd417bdfe6dec..e7eb10362b02d07fcade190aa0a4c097c8518de9 100644 (file)
@@ -1,10 +1,11 @@
-import re
 from datetime import timedelta
 
 import pytest
 
 from .conf import TlsTestConf
+from pyhttpd.env import HttpdTestEnv
 
+@pytest.mark.skipif(condition=not HttpdTestEnv.has_shared_module("tls"), reason="no mod_tls available")
 
 class TestProxyTLS:
 
@@ -53,6 +54,13 @@ class TestProxyTLS:
         # does not work, since SSLProxy* not configured
         data = env.tls_get_json(env.domain_b, "/proxy-local/index.json")
         assert data is None
+        #
+        env.httpd_error_log.ignore_recent(
+            lognos = [
+                "AH01961",  # failed to enable ssl support [Hint: if using mod_ssl, see SSLProxyEngine]
+                "AH00961"   # failed to enable ssl support (mod_proxy)
+            ]
+        )
 
     def test_tls_15_proxy_tls_h2_get(self, env):
         r = env.tls_get(env.domain_b, "/proxy-h2-tls/index.json")
index ca082362d7a9a913bc2249b5a1b976c0137454c2..88b351fd94b8ef38b3dc01f0973df856588e8874 100644 (file)
@@ -3,6 +3,9 @@ import time
 import pytest
 
 from .conf import TlsTestConf
+from pyhttpd.env import HttpdTestEnv
+
+@pytest.mark.skipif(condition=not HttpdTestEnv.has_shared_module("tls"), reason="no mod_tls available")
 
 
 class TestProxyMixed:
index 7b5ef44d6fb9d577df1b4495134240b1c5fcee80..a5410d63ad4c7468a30bcddec07d874dfb666f86 100644 (file)
@@ -3,8 +3,9 @@ import os
 import pytest
 
 from .conf import TlsTestConf
+from pyhttpd.env import HttpdTestEnv
 
-
+@pytest.mark.skipif(condition=not HttpdTestEnv.has_shared_module("tls"), reason="no mod_tls available")
 class TestProxyMachineCert:
 
     @pytest.fixture(autouse=True, scope='class')
index cd3363fb73a75b4a1d2f39f0ff9f028b54ccc140..e1c6bf5ee959972fb2da94e884dc15c2f063fd1a 100644 (file)
@@ -26,15 +26,96 @@ class HttpdConf(object):
     def install(self):
         self.env.install_test_conf(self._lines)
 
+    def replacetlsstr(self, line):
+            l = line.replace("TLS_", "")
+            l = l.replace("\n", " ")
+            l = l.replace("\\", " ")
+            l = " ".join(l.split())
+            l = l.replace(" ", ":")
+            l = l.replace("_", "-")
+            l = l.replace("-WITH", "")
+            l = l.replace("AES-", "AES")
+            l = l.replace("POLY1305-SHA256", "POLY1305")
+            return l
+
+    def replaceinstr(self, line):
+        if line.startswith("TLSCiphersPrefer"):
+            # the "TLS_" are changed into "".
+            l = self.replacetlsstr(line)
+            l = l.replace("TLSCiphersPrefer:", "SSLCipherSuite ")
+        elif line.startswith("TLSCiphersSuppress"):
+            # like SSLCipherSuite but with :!
+            l = self.replacetlsstr(line)
+            l = l.replace("TLSCiphersSuppress:", "SSLCipherSuite !")
+            l = l.replace(":", ":!")
+        elif line.startswith("TLSCertificate"):
+            l = line.replace("TLSCertificate", "SSLCertificateFile")
+        elif line.startswith("TLSProtocol"):
+            # mod_ssl is different (+ no supported and 0x code have to be translated)
+            l = line.replace("TLSProtocol", "SSLProtocol")
+            l = l.replace("+", "")
+            l = l.replace("default", "all")
+            l = l.replace("0x0303", "1.2") # need to check 1.3 and 1.1
+        elif line.startswith("SSLProtocol"):
+            l = line # we have that in test/modules/tls/test_05_proto.py
+        elif line.startswith("TLSHonorClientOrder"):
+            # mod_ssl has SSLHonorCipherOrder on = use server off = use client.
+            l = line.lower()
+            if "on" in l:
+                l = "SSLHonorCipherOrder off"
+            else:
+                l = "SSLHonorCipherOrder on"
+        elif line.startswith("TLSEngine"):
+            # In fact it should go in the corresponding VirtualHost... Not sure how to do that.
+            l = "SSLEngine On"
+        else:
+            if line != "":
+                l = line.replace("TLS", "SSL")
+            else:
+                l = line
+        return l
+
     def add(self, line: Any):
+        # make we transform the TLS to SSL if we are using mod_ssl
         if isinstance(line, str):
+            if not HttpdTestEnv.has_shared_module("tls"):
+                line = self.replaceinstr(line)
             if self._indents > 0:
                 line = f"{'  ' * self._indents}{line}"
             self._lines.append(line)
         else:
-            if self._indents > 0:
-                line = [f"{'  ' * self._indents}{l}" for l in line]
-            self._lines.extend(line)
+            if not HttpdTestEnv.has_shared_module("tls"):
+                new = []
+                previous = ""
+                for l in line:
+                    if previous.startswith("SSLCipherSuite"):
+                        if l.startswith("TLSCiphersPrefer") or l.startswith("TLSCiphersSuppress"):
+                            # we need to merge it   
+                            l = self.replaceinstr(l)
+                            l = l.replace("SSLCipherSuite ", ":")
+                            previous = previous + l
+                            continue
+                        else:
+                            if self._indents > 0:
+                                previous = f"{'  ' * self._indents}{previous}"
+                            new.append(previous)
+                            previous = ""
+                    l = self.replaceinstr(l)
+                    if l.startswith("SSLCipherSuite"):
+                        previous = l
+                        continue
+                    if self._indents > 0:
+                        l = f"{'  ' * self._indents}{l}"
+                    new.append(l)
+                if previous != "":
+                    if self._indents > 0:
+                        previous = f"{'  ' * self._indents}{previous}"
+                    new.append(previous)
+                self._lines.extend(new)
+            else:
+                if self._indents > 0:
+                    line = [f"{'  ' * self._indents}{l}" for l in line]
+                self._lines.extend(line)
         return self
 
     def add_certificate(self, cert_file, key_file, ssl_module=None):
index 3d7993ffe13960def434a1258e459e39168b1de9..7dcc25bcc80c0ddede8d4ad87292cf3cf994541c 100644 (file)
@@ -131,8 +131,6 @@ class CurlPiper:
             recv_deltas.append(datetime.timedelta(microseconds=delta_mics))
             last_mics = mics
         stutter_td = datetime.timedelta(seconds=stutter.total_seconds() * 0.75)  # 25% leeway
-        # TODO: the first two chunks are often close together, it seems
-        # there still is a little buffering delay going on
         for idx, td in enumerate(recv_deltas[1:]):
             assert stutter_td < td, \
                 f"chunk {idx} arrived too early \n{recv_deltas}\nafter {td}\n{recv_err}"
index 1d4e8b1c453d291cac0e9417635ca4b55a1d7ba4..8a20d928432d226c821f79ded09ff42baaa9de24 100644 (file)
@@ -93,6 +93,7 @@ class HttpdTestSetup:
         self._make_modules_conf()
         self._make_htdocs()
         self._add_aptest()
+        self._build_clients()
         self.env.clear_curl_headerfiles()
 
     def _make_dirs(self):
@@ -196,6 +197,16 @@ class HttpdTestSetup:
             # load our test module which is not installed
             fd.write(f"LoadModule aptest_module   \"{local_dir}/mod_aptest/.libs/mod_aptest.so\"\n")
 
+    def _build_clients(self):
+        clients_dir = os.path.join(
+            os.path.dirname(os.path.dirname(inspect.getfile(HttpdTestSetup))),
+            'clients')
+        p = subprocess.run(['make'], capture_output=True, cwd=clients_dir)
+        rv = p.returncode
+        if rv != 0:
+            log.error(f"compiling test clients failed: {p.stderr}")
+            raise Exception(f"compiling test clients failed: {p.stderr}")
+
 
 class HttpdTestEnv:
 
@@ -324,6 +335,12 @@ class HttpdTestEnv:
             for name in self._httpd_log_modules:
                 self._log_interesting += f" {name}:{log_level}"
 
+    def check_error_log(self):
+        errors, warnings = self._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))
+
     @property
     def curl(self) -> str:
         return self._curl
@@ -572,16 +589,22 @@ class HttpdTestEnv:
         return f"{scheme}://{hostname}.{self.http_tld}:{port}{path}"
 
     def install_test_conf(self, lines: List[str]):
+        self.apache_stop()
         with open(self._test_conf, 'w') as fd:
             fd.write('\n'.join(self._httpd_base_conf))
             fd.write('\n')
             fd.write(f"CoreDumpDirectory {self._server_dir}\n")
-            if self._verbosity >= 2:
-                fd.write(f"LogLevel core:trace5 {self.mpm_module}:trace5 http:trace5\n")
+            fd.write('\n')
             if self._verbosity >= 3:
-                fd.write(f"LogLevel dumpio:trace7\n")
+                fd.write(f"LogLevel trace7 ssl:trace6\n")
                 fd.write(f"DumpIoOutput on\n")
                 fd.write(f"DumpIoInput on\n")
+            elif self._verbosity >= 2:
+                fd.write(f"LogLevel debug core:trace5 {self.mpm_module}:trace5 ssl:trace5 http:trace5\n")
+            elif self._verbosity >= 1:
+                fd.write(f"LogLevel info\n")
+            else:
+                fd.write(f"LogLevel warn\n")
             if self._log_interesting:
                 fd.write(self._log_interesting)
             fd.write('\n\n')
index dff7623b246ad51ee1d3ef03a9575fe698424d08..17b0502e9dee5b88a6b4f0279e62dc4d63da8fa8 100644 (file)
@@ -8,33 +8,32 @@ from typing import List, Tuple, Any
 
 class HttpdErrorLog:
     """Checking the httpd error log for errors and warnings, including
-       limiting checks from a last known position forward.
+       limiting checks from a recent known position forward.
     """
 
-    RE_ERRLOG_ERROR = re.compile(r'.*\[(?P<module>[^:]+):error].*')
-    RE_ERRLOG_WARN = re.compile(r'.*\[(?P<module>[^:]+):warn].*')
-    RE_APLOGNO = re.compile(r'.*\[(?P<module>[^:]+):(error|warn)].* (?P<aplogno>AH\d+): .+')
-    RE_SSL_LIB_ERR = re.compile(r'.*\[ssl:error].* SSL Library Error: error:(?P<errno>\S+):.+')
+    RE_ERRLOG_WARN = re.compile(r'.*\[[^:]+:warn].*')
+    RE_ERRLOG_ERROR = re.compile(r'.*\[[^:]+:error].*')
+    RE_APLOGNO = re.compile(r'.*\[[^:]+:(error|warn)].* (?P<aplogno>AH\d+): .+')
 
     def __init__(self, path: str):
         self._path = path
-        self._ignored_modules = []
+        self._ignored_matches = []
         self._ignored_lognos = set()
-        self._ignored_patterns = []
         # remember the file position we started with
         self._start_pos = 0
         if os.path.isfile(self._path):
             with open(self._path) as fd:
                 self._start_pos = fd.seek(0, SEEK_END)
-        self._last_pos = self._start_pos
-        self._last_errors = []
-        self._last_warnings = []
-        self._observed_erros = set()
-        self._observed_warnings = set()
+        self._recent_pos = self._start_pos
+        self._recent_errors = []
+        self._recent_warnings = []
+        self._caught_errors = set()
+        self._caught_warnings = set()
+        self._caught_matches = set()
 
     def __repr__(self):
-        return f"HttpdErrorLog[{self._path}, errors: {' '.join(self._last_errors)}, " \
-               f"warnings: {' '.join(self._last_warnings)}]"
+        return f"HttpdErrorLog[{self._path}, errors: {' '.join(self._recent_errors)}, " \
+               f"warnings: {' '.join(self._recent_warnings)}]"
 
     @property
     def path(self) -> str:
@@ -42,118 +41,108 @@ class HttpdErrorLog:
 
     def clear_log(self):
         if os.path.isfile(self.path):
-            os.remove(self.path)
-        self._start_pos = 0
-        self._last_pos = self._start_pos
-        self._last_errors = []
-        self._last_warnings = []
-        self._observed_erros = set()
-        self._observed_warnings = set()
+            os.truncate(self.path, 0)
+        self._start_pos = self._recent_pos = 0
+        self._recent_errors = []
+        self._recent_warnings = []
+        self._caught_errors = set()
+        self._caught_warnings = set()
+        self._caught_matches = set()
+
+    def _lookup_matches(self, line: str, matches: List[str]) -> bool:
+        for m in matches:
+            if re.match(m, line):
+                return True
+        return False
+
+    def _lookup_lognos(self, line: str, lognos: set) -> bool:
+        if len(lognos) > 0:
+            m = self.RE_APLOGNO.match(line)
+            if m and m.group('aplogno') in lognos:
+                return True
+        return False
 
-    def set_ignored_modules(self, modules: List[str]):
-        self._ignored_modules = modules.copy() if modules else []
+    def clear_ignored_matches(self):
+        self._ignored_matches = []
 
-    def set_ignored_lognos(self, lognos: List[str]):
-        if lognos:
-            for l in lognos:
-                self._ignored_lognos.add(l)
+    def add_ignored_matches(self, matches: List[str]):
+        for m in matches:
+            self._ignored_matches.append(re.compile(m))
 
-    def add_ignored_patterns(self, patterns: List[Any]):
-        self._ignored_patterns.extend(patterns)
+    def clear_ignored_lognos(self):
+        self._ignored_lognos = set()
+
+    def add_ignored_lognos(self, lognos: List[str]):
+        for l in lognos:
+            self._ignored_lognos.add(l)
 
     def _is_ignored(self, line: str) -> bool:
-        for p in self._ignored_patterns:
-            if p.match(line):
-                return True
-        m = self.RE_APLOGNO.match(line)
-        if m and m.group('aplogno') in self._ignored_lognos:
+        if self._lookup_matches(line, self._ignored_matches):
+            return True
+        if self._lookup_lognos(line, self._ignored_lognos):
             return True
         return False
 
-    def get_recent(self, advance=True) -> Tuple[List[str], List[str]]:
-        """Collect error and warning from the log since the last remembered position
-        :param advance: advance the position to the end of the log afterwards
-        :return: list of error and list of warnings as tuple
-        """
-        self._last_errors = []
-        self._last_warnings = []
+    def ignore_recent(self, lognos: List[str] = [], matches: List[str] = []):
+        """After a test case triggered errors/warnings on purpose, add
+           those to our 'caught' list so the do not get reported as 'missed'.
+           """
+        self._recent_errors = []
+        self._recent_warnings = []
         if os.path.isfile(self._path):
             with open(self._path) as fd:
-                fd.seek(self._last_pos, os.SEEK_SET)
+                fd.seek(self._recent_pos, os.SEEK_SET)
+                lognos_set = set(lognos)
                 for line in fd:
                     if self._is_ignored(line):
                         continue
-                    m = self.RE_ERRLOG_ERROR.match(line)
-                    if m and m.group('module') not in self._ignored_modules:
-                        self._last_errors.append(line)
+                    if self._lookup_matches(line, matches):
+                        self._caught_matches.add(line)
                         continue
                     m = self.RE_ERRLOG_WARN.match(line)
-                    if m:
-                        if m and m.group('module') not in self._ignored_modules:
-                            self._last_warnings.append(line)
-                            continue
-                if advance:
-                    self._last_pos = fd.tell()
-            self._observed_erros.update(set(self._last_errors))
-            self._observed_warnings.update(set(self._last_warnings))
-        return self._last_errors, self._last_warnings
-
-    def get_recent_count(self, advance=True):
-        errors, warnings = self.get_recent(advance=advance)
-        return len(errors), len(warnings)
-
-    def ignore_recent(self):
-        """After a test case triggered errors/warnings on purpose, add
-           those to our 'observed' list so the do not get reported as 'missed'.
-           """
-        self._last_errors = []
-        self._last_warnings = []
-        if os.path.isfile(self._path):
-            with open(self._path) as fd:
-                fd.seek(self._last_pos, os.SEEK_SET)
-                for line in fd:
-                    if self._is_ignored(line):
+                    if m and self._lookup_lognos(line, lognos_set):
+                        self._caught_warnings.add(line)
                         continue
                     m = self.RE_ERRLOG_ERROR.match(line)
-                    if m and m.group('module') not in self._ignored_modules:
-                        self._observed_erros.add(line)
+                    if m and self._lookup_lognos(line, lognos_set):
+                        self._caught_errors.add(line)
                         continue
-                    m = self.RE_ERRLOG_WARN.match(line)
-                    if m:
-                        if m and m.group('module') not in self._ignored_modules:
-                            self._observed_warnings.add(line)
-                            continue
-                self._last_pos = fd.tell()
+                self._recent_pos = fd.tell()
 
     def get_missed(self) -> Tuple[List[str], List[str]]:
         errors = []
         warnings = []
+        self._recent_errors = []
+        self._recent_warnings = []
         if os.path.isfile(self._path):
             with open(self._path) as fd:
                 fd.seek(self._start_pos, os.SEEK_SET)
                 for line in fd:
                     if self._is_ignored(line):
                         continue
+                    if line in self._caught_matches:
+                        continue
+                    m = self.RE_ERRLOG_WARN.match(line)
+                    if m and line not in self._caught_warnings:
+                        warnings.append(line)
+                        continue
                     m = self.RE_ERRLOG_ERROR.match(line)
-                    if m and m.group('module') not in self._ignored_modules \
-                            and line not in self._observed_erros:
+                    if m and line not in self._caught_errors:
                         errors.append(line)
                         continue
-                    m = self.RE_ERRLOG_WARN.match(line)
-                    if m:
-                        if m and m.group('module') not in self._ignored_modules \
-                                and line not in self._observed_warnings:
-                            warnings.append(line)
-                            continue
+                self._start_pos = self._recent_pos = fd.tell()
+        self._caught_errors = set()
+        self._caught_warnings = set()
+        self._caught_matches = set()
         return errors, warnings
 
-    def scan_recent(self, pattern: re, timeout=10):
+    def scan_recent(self, pattern: re.Pattern, timeout=10):
         if not os.path.isfile(self.path):
             return False
         with open(self.path) as fd:
             end = datetime.now() + timedelta(seconds=timeout)
             while True:
-                fd.seek(self._last_pos, os.SEEK_SET)
+                fd.seek(self._recent_pos, os.SEEK_SET)
                 for line in fd:
                     if pattern.match(line):
                         return True