diff options
Diffstat (limited to '')
63 files changed, 1788 insertions, 200 deletions
diff --git a/test/modules/core/conftest.py b/test/modules/core/conftest.py index 439cd22..22906ef 100644 --- a/test/modules/core/conftest.py +++ b/test/modules/core/conftest.py @@ -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 index 0000000..9c63380 --- /dev/null +++ b/test/modules/core/env.py @@ -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)) diff --git a/test/modules/core/test_001_encoding.py b/test/modules/core/test_001_encoding.py index b7ffbaa..a3b24d0 100644 --- a/test/modules/core/test_001_encoding.py +++ b/test/modules/core/test_001_encoding.py @@ -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 index 0000000..cf203bc --- /dev/null +++ b/test/modules/core/test_002_restarts.py @@ -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 index 0000000..e69de29 --- /dev/null +++ b/test/modules/http1/__init__.py diff --git a/test/modules/http1/conftest.py b/test/modules/http1/conftest.py new file mode 100644 index 0000000..33a16a1 --- /dev/null +++ b/test/modules/http1/conftest.py @@ -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 index 0000000..e2df1a5 --- /dev/null +++ b/test/modules/http1/env.py @@ -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 index 0000000..e69de29 --- /dev/null +++ b/test/modules/http1/htdocs/cgi/files/empty.txt diff --git a/test/modules/http1/htdocs/cgi/hello.py b/test/modules/http1/htdocs/cgi/hello.py new file mode 100755 index 0000000..191acb2 --- /dev/null +++ b/test/modules/http1/htdocs/cgi/hello.py @@ -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 index 0000000..c7e0648 --- /dev/null +++ b/test/modules/http1/htdocs/cgi/requestparser.py @@ -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 index 0000000..632b7e9 --- /dev/null +++ b/test/modules/http1/htdocs/cgi/upload.py @@ -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 index 0000000..cbd87b5 --- /dev/null +++ b/test/modules/http1/mod_h1test/mod_h1test.c @@ -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 index 0000000..e69de29 --- /dev/null +++ b/test/modules/http1/mod_h1test/mod_h1test.slo diff --git a/test/modules/http1/test_001_alive.py b/test/modules/http1/test_001_alive.py new file mode 100644 index 0000000..0a1de1d --- /dev/null +++ b/test/modules/http1/test_001_alive.py @@ -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 index 0000000..1cd5917 --- /dev/null +++ b/test/modules/http1/test_003_get.py @@ -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 index 0000000..005a8c2 --- /dev/null +++ b/test/modules/http1/test_004_post.py @@ -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 index 0000000..ca717a0 --- /dev/null +++ b/test/modules/http1/test_005_trailers.py @@ -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 index 0000000..eb83217 --- /dev/null +++ b/test/modules/http1/test_006_unsafe.py @@ -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 index 0000000..7c52f68 --- /dev/null +++ b/test/modules/http1/test_007_strict.py @@ -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 diff --git a/test/modules/http2/conftest.py b/test/modules/http2/conftest.py index 55d0c3a..118cef1 100644 --- a/test/modules/http2/conftest.py +++ b/test/modules/http2/conftest.py @@ -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)) - diff --git a/test/modules/http2/env.py b/test/modules/http2/env.py index 34d196d..b2443e0 100644 --- a/test/modules/http2/env.py +++ b/test/modules/http2/env.py @@ -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)) diff --git a/test/modules/http2/test_007_ssi.py b/test/modules/http2/test_007_ssi.py index 97e38df..f5411bc 100644 --- a/test/modules/http2/test_007_ssi.py +++ b/test/modules/http2/test_007_ssi.py @@ -1,4 +1,3 @@ -import re import pytest from .env import H2Conf, H2TestEnv diff --git a/test/modules/http2/test_008_ranges.py b/test/modules/http2/test_008_ranges.py index 4dcdcc8..dd695bb 100644 --- a/test/modules/http2/test_008_ranges.py +++ b/test/modules/http2/test_008_ranges.py @@ -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']) diff --git a/test/modules/http2/test_100_conn_reuse.py b/test/modules/http2/test_100_conn_reuse.py index 3ebac24..103166f 100644 --- a/test/modules/http2/test_100_conn_reuse.py +++ b/test/modules/http2/test_100_conn_reuse.py @@ -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 + ] + ) diff --git a/test/modules/http2/test_101_ssl_reneg.py b/test/modules/http2/test_101_ssl_reneg.py index 528002f..d278af2 100644 --- a/test/modules/http2/test_101_ssl_reneg.py +++ b/test/modules/http2/test_101_ssl_reneg.py @@ -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.*' + ] + ) diff --git a/test/modules/http2/test_102_require.py b/test/modules/http2/test_102_require.py index b7e4eae..4b0cad5 100644 --- a/test/modules/http2/test_102_require.py +++ b/test/modules/http2/test_102_require.py @@ -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 + ] + ) diff --git a/test/modules/http2/test_103_upgrade.py b/test/modules/http2/test_103_upgrade.py index 2fa7d1d..1542450 100644 --- a/test/modules/http2/test_103_upgrade.py +++ b/test/modules/http2/test_103_upgrade.py @@ -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 diff --git a/test/modules/http2/test_105_timeout.py b/test/modules/http2/test_105_timeout.py index f7d3859..22160b4 100644 --- a/test/modules/http2/test_105_timeout.py +++ b/test/modules/http2/test_105_timeout.py @@ -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>. diff --git a/test/modules/http2/test_106_shutdown.py b/test/modules/http2/test_106_shutdown.py index 83e143c..fab881b 100644 --- a/test/modules/http2/test_106_shutdown.py +++ b/test/modules/http2/test_106_shutdown.py @@ -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 diff --git a/test/modules/http2/test_200_header_invalid.py b/test/modules/http2/test_200_header_invalid.py index 5b3aafd..04c022c 100644 --- a/test/modules/http2/test_200_header_invalid.py +++ b/test/modules/http2/test_200_header_invalid.py @@ -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): diff --git a/test/modules/http2/test_203_rfc9113.py b/test/modules/http2/test_203_rfc9113.py index 9fc8f3b..1fe3e13 100644 --- a/test/modules/http2/test_203_rfc9113.py +++ b/test/modules/http2/test_203_rfc9113.py @@ -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) diff --git a/test/modules/http2/test_500_proxy.py b/test/modules/http2/test_500_proxy.py index 88a8ece..87e523c 100644 --- a/test/modules/http2/test_500_proxy.py +++ b/test/modules/http2/test_500_proxy.py @@ -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 + ] + ) diff --git a/test/modules/http2/test_600_h2proxy.py b/test/modules/http2/test_600_h2proxy.py index 18d5d1d..18a528e 100644 --- a/test/modules/http2/test_600_h2proxy.py +++ b/test/modules/http2/test_600_h2proxy.py @@ -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() diff --git a/test/modules/http2/test_700_load_get.py b/test/modules/http2/test_700_load_get.py index 78760fb..138e74c 100644 --- a/test/modules/http2/test_700_load_get.py +++ b/test/modules/http2/test_700_load_get.py @@ -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 diff --git a/test/modules/http2/test_712_buffering.py b/test/modules/http2/test_712_buffering.py index 6658441..0a6978b 100644 --- a/test/modules/http2/test_712_buffering.py +++ b/test/modules/http2/test_712_buffering.py @@ -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) diff --git a/test/modules/http2/test_800_websockets.py b/test/modules/http2/test_800_websockets.py index 52af1a3..c0fc0c2 100644 --- a/test/modules/http2/test_800_websockets.py +++ b/test/modules/http2/test_800_websockets.py @@ -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): diff --git a/test/modules/md/conftest.py b/test/modules/md/conftest.py index 04165a2..0f9e4a9 100755 --- a/test/modules/md/conftest.py +++ b/test/modules/md/conftest.py @@ -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): diff --git a/test/modules/md/test_300_conf_validate.py b/test/modules/md/test_300_conf_validate.py index 85371ba..88df168 100644 --- a/test/modules/md/test_300_conf_validate.py +++ b/test/modules/md/test_300_conf_validate.py @@ -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.*' + ]) + diff --git a/test/modules/md/test_702_auto.py b/test/modules/md/test_702_auto.py index 8e8f5f1..04a9c75 100644 --- a/test/modules/md/test_702_auto.py +++ b/test/modules/md/test_702_auto.py @@ -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): diff --git a/test/modules/md/test_720_wildcard.py b/test/modules/md/test_720_wildcard.py index 23b311c..916c47a 100644 --- a/test/modules/md/test_720_wildcard.py +++ b/test/modules/md/test_720_wildcard.py @@ -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): diff --git a/test/modules/md/test_730_static.py b/test/modules/md/test_730_static.py index f7f7b4b..891ae62 100644 --- a/test/modules/md/test_730_static.py +++ b/test/modules/md/test_730_static.py @@ -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 + ] + ) diff --git a/test/modules/md/test_740_acme_errors.py b/test/modules/md/test_740_acme_errors.py index 670c9ab..364aaca 100644 --- a/test/modules/md/test_740_acme_errors.py +++ b/test/modules/md/test_740_acme_errors.py @@ -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.*' + ] + ) diff --git a/test/modules/md/test_741_setup_errors.py b/test/modules/md/test_741_setup_errors.py index 49b4e78..9ad79f0 100644 --- a/test/modules/md/test_741_setup_errors.py +++ b/test/modules/md/test_741_setup_errors.py @@ -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.*' + ] + ) diff --git a/test/modules/md/test_750_eab.py b/test/modules/md/test_750_eab.py index af1be95..aec7e89 100644 --- a/test/modules/md/test_750_eab.py +++ b/test/modules/md/test_750_eab.py @@ -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 diff --git a/test/modules/md/test_780_tailscale.py b/test/modules/md/test_780_tailscale.py index 84a266b..27a2df4 100644 --- a/test/modules/md/test_780_tailscale.py +++ b/test/modules/md/test_780_tailscale.py @@ -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 + ] + ) diff --git a/test/modules/md/test_790_failover.py b/test/modules/md/test_790_failover.py index a939912..696161f 100644 --- a/test/modules/md/test_790_failover.py +++ b/test/modules/md/test_790_failover.py @@ -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 .*' + ] + ) diff --git a/test/modules/md/test_900_notify.py b/test/modules/md/test_900_notify.py index 30e0742..9d18da5 100644 --- a/test/modules/md/test_900_notify.py +++ b/test/modules/md/test_900_notify.py @@ -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): diff --git a/test/modules/md/test_901_message.py b/test/modules/md/test_901_message.py index 8d03bfd..b18cfd3 100644 --- a/test/modules/md/test_901_message.py +++ b/test/modules/md/test_901_message.py @@ -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\].*' + ] + ) diff --git a/test/modules/md/test_920_status.py b/test/modules/md/test_920_status.py index c89ce6d..6ad7087 100644 --- a/test/modules/md/test_920_status.py +++ b/test/modules/md/test_920_status.py @@ -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.*' + ] + ) diff --git a/test/modules/proxy/conftest.py b/test/modules/proxy/conftest.py index 23c5f14..7e6f4e7 100644 --- a/test/modules/proxy/conftest.py +++ b/test/modules/proxy/conftest.py @@ -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)) diff --git a/test/modules/proxy/env.py b/test/modules/proxy/env.py index 9ed635c..098d4d4 100644 --- a/test/modules/proxy/env.py +++ b/test/modules/proxy/env.py @@ -1,7 +1,6 @@ import inspect import logging import os -import re import subprocess from typing import Dict, Any diff --git a/test/modules/proxy/test_02_unix.py b/test/modules/proxy/test_02_unix.py index 7f3d4d5..0c39bc9 100644 --- a/test/modules/proxy/test_02_unix.py +++ b/test/modules/proxy/test_02_unix.py @@ -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 diff --git a/test/modules/tls/conf.py b/test/modules/tls/conf.py index ddeb91f..b34f746 100644 --- a/test/modules/tls/conf.py +++ b/test/modules/tls/conf.py @@ -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([ diff --git a/test/modules/tls/conftest.py b/test/modules/tls/conftest.py index cde4be6..c7cb858 100644 --- a/test/modules/tls/conftest.py +++ b/test/modules/tls/conftest.py @@ -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 diff --git a/test/modules/tls/env.py b/test/modules/tls/env.py index 0e457bf..6afc472 100644 --- a/test/modules/tls/env.py +++ b/test/modules/tls/env.py @@ -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): diff --git a/test/modules/tls/test_02_conf.py b/test/modules/tls/test_02_conf.py index 4d6aa60..88be80c 100644 --- a/test/modules/tls/test_02_conf.py +++ b/test/modules/tls/test_02_conf.py @@ -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 diff --git a/test/modules/tls/test_03_sni.py b/test/modules/tls/test_03_sni.py index cf421c0..cbd142a 100644 --- a/test/modules/tls/test_03_sni.py +++ b/test/modules/tls/test_03_sni.py @@ -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): diff --git a/test/modules/tls/test_06_ciphers.py b/test/modules/tls/test_06_ciphers.py index 2e60bdd..4bedd69 100644 --- a/test/modules/tls/test_06_ciphers.py +++ b/test/modules/tls/test_06_ciphers.py @@ -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 diff --git a/test/modules/tls/test_08_vars.py b/test/modules/tls/test_08_vars.py index a8df99a..0e3ee74 100644 --- a/test/modules/tls/test_08_vars.py +++ b/test/modules/tls/test_08_vars.py @@ -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 diff --git a/test/modules/tls/test_14_proxy_ssl.py b/test/modules/tls/test_14_proxy_ssl.py index cefcbf6..87e04c2 100644 --- a/test/modules/tls/test_14_proxy_ssl.py +++ b/test/modules/tls/test_14_proxy_ssl.py @@ -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 diff --git a/test/modules/tls/test_15_proxy_tls.py b/test/modules/tls/test_15_proxy_tls.py index f2f670d..e7eb103 100644 --- a/test/modules/tls/test_15_proxy_tls.py +++ b/test/modules/tls/test_15_proxy_tls.py @@ -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") diff --git a/test/modules/tls/test_16_proxy_mixed.py b/test/modules/tls/test_16_proxy_mixed.py index ca08236..88b351f 100644 --- a/test/modules/tls/test_16_proxy_mixed.py +++ b/test/modules/tls/test_16_proxy_mixed.py @@ -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: diff --git a/test/modules/tls/test_17_proxy_machine_cert.py b/test/modules/tls/test_17_proxy_machine_cert.py index 7b5ef44..a5410d6 100644 --- a/test/modules/tls/test_17_proxy_machine_cert.py +++ b/test/modules/tls/test_17_proxy_machine_cert.py @@ -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') |