diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 0000000..e5d91d9 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,129 @@ +# Copyright 2025 Stefan Eissing (https://dev-icing.de) +# +# Licensed 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. +# + +name: Linux + +'on': + push: + branches: + - master + - '*/ci' + paths-ignore: + - '**/*.md' + pull_request: + branches: + - master + paths-ignore: + - '**/*.md' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +permissions: {} + +env: + MARGS: "-j5" + CFLAGS: "-g" + +jobs: + linux: + name: ${{ matrix.build.name }} (rustls-ffi ${{matrix.rustls-version}} ${{ matrix.crypto }} ${{matrix.rust}}) + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + rust: + - stable + - nightly + crypto: + - ring + # aws-lc-sys v0.21.1 is not building due to compiler warnings + # - aws-lc-rs + rustls-version: + - v0.14.1 + - main + build: + - name: mod_tls + install_packages: + + steps: + - name: 'install prereqs' + run: | + sudo apt-get update -y + sudo apt-get install -y --no-install-suggests --no-install-recommends \ + libtool autoconf automake pkgconf cmake apache2 apache2-dev openssl \ + curl nghttp2-client libssl-dev \ + ${{ matrix.build.install_packages }} + python3 -m venv $HOME/venv + + - uses: actions/checkout@v4 + + - name: Install ${{ matrix.rust }} toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: 'checkout rustls-ffi' + run: | + cd $HOME/ + git clone --quiet --depth=1 -b ${{ matrix.rustls-version }} --recursive https://github.com/rustls/rustls-ffi.git + + - name: 'build rustls-ffi (Makefile)' + if: matrix.rustls-version != 'main' + run: | + cd $HOME/rustls-ffi + make DESTDIR=$HOME/rustls-ffi/build/rust CRYPTO_PROVIDER=${{ matrix.crypto }} install + + - name: Install cargo-c + if: matrix.rustls-version == 'main' + env: + # Version picked for MSRV compat. + LINK: https://github.com/lu-zero/cargo-c/releases/latest/download/ + CARGO_C_FILE: cargo-c-x86_64-unknown-linux-musl.tar.gz + run: | + curl -L $LINK/$CARGO_C_FILE | tar xz -C ~/.cargo/bin + + - name: 'build rustls-ffi (cmake)' + if: matrix.rustls-version == 'main' + run: | + cd $HOME/rustls-ffi + cmake \ + -DCRYPTO_PROVIDER=${{matrix.crypto}} \ + -DDYN_LINK=on \ + -DCMAKE_BUILD_TYPE=Release \ + -S librustls -B build + cmake --build build --config "Release" + + - name: 'install test prereqs' + run: | + [ -x "$HOME/venv/bin/activate" ] && source $HOME/venv/bin/activate + python3 -m pip install -r test/requirements.txt + + - name: 'configure' + run: | + autoreconf -fi + ./configure --enable-werror --with-rustls=$HOME/rustls-ffi/build/rust + + - name: 'build' + run: make V=1 + + - name: pytest + env: + PYTEST_ADDOPTS: "--color=yes" + run: | + [ -x "$HOME/venv/bin/activate" ] && source $HOME/venv/bin/activate + pytest -v diff --git a/configure.ac b/configure.ac index 5390355..7e77d12 100644 --- a/configure.ac +++ b/configure.ac @@ -181,24 +181,43 @@ CPPFLAGS="-I$($APXS -q includedir) -I$($APXS -q APR_INCLUDEDIR) $($APXS -q EXTRA HTTPD_VERSION="$($APXS -q HTTPD_VERSION)" AC_SUBST(HTTPD_VERSION) -APACHECTL="$sbindir/apachectl" -if test ! -x "$APACHECTL"; then - # rogue distros rename things! =) - APACHECTL="$sbindir/apache2ctl" +HTTPD="$sbindir/httpd" +if test -x "$HTTPD"; then + : # all fine +else + HTTPD="$sbindir/apache2" + if test -x "$HTTPD"; then + : # all fine + else + HTTPD="" + AC_PATH_PROG([HTTPD], [httpd]) + if test -x "$HTTPD"; then + : # ok + else + HTTPD="" + AC_PATH_PROG([HTTPD], [apache2]) + if test -x "$HTTPD"; then + : # ok + else + AC_MSG_ERROR([httpd/apache2 not in PATH]) + fi + fi + fi fi -AC_SUBST(APACHECTL) -if test -x "$APACHECTL"; then - DSO_MODULES="$($APACHECTL -t -D DUMP_MODULES | fgrep '(shared)'| sed 's/_module.*//g'|tr -d \\n)" +if test -x "$HTTPD"; then + DSO_MODULES="$($HTTPD -t -D DUMP_MODULES | fgrep '(shared)'| sed 's/_module.*//g'|tr -d \\n)" AC_SUBST(DSO_MODULES) - STATIC_MODULES="$($APACHECTL -t -D DUMP_MODULES | fgrep '(static)'| sed 's/_module.*//g'|tr -d \\n)" + STATIC_MODULES="$($HTTPD -t -D DUMP_MODULES | fgrep '(static)'| sed 's/_module.*//g'|tr -d \\n)" AC_SUBST(STATIC_MODULES) MPM_MODULES="mpm_event mpm_worker" AC_SUBST(MPM_MODULES) else - AC_MSG_WARN("apachectl not found in '$BINDIR', test suite will not work!") - APACHECTL="" + AC_MSG_WARN("httpd/apache2 not found, test suite will not work!") + HTTPD="" fi +AC_SUBST(HTTPD) + AC_SUBST(LOAD_LOG_CONFIG) AC_SUBST(LOAD_LOGIO) AC_SUBST(LOAD_UNIXD) @@ -208,18 +227,18 @@ AC_SUBST(LOAD_WATCHDOG) export BUILD_SUBDIRS="src" if test x"$request_rustls" != "xcheck"; then - LDFLAGS="$LDFLAGS -L$request_rustls/lib"; + LDFLAGS="$LDFLAGS -L$request_rustls/lib -Wl,-rpath,$request_rustls/lib"; CFLAGS="$CFLAGS -I$request_rustls/include"; CPPFLAGS="$CPPFLAGS -I$request_rustls/include"; fi # Need some additional things for rustls linkage. This seems platform specific. if test $(uname) = "Darwin"; then - CRUSTLS_LDFLAGS="-Wl,-dead_strip -framework Security -framework Foundation" + RUSTLS_LDFLAGS="-Wl,-dead_strip -framework Security -framework Foundation" else - CRUSTLS_LDFLAGS="-Wl,--gc-sections -lpthread -ldl" + RUSTLS_LDFLAGS="-Wl,--gc-sections -lpthread -ldl" fi -LDFLAGS="$LDFLAGS $CRUSTLS_LDFLAGS" +LDFLAGS="$LDFLAGS $RUSTLS_LDFLAGS" # verify that we can link rustls now # commented: problem running on debian @@ -296,6 +315,7 @@ AC_MSG_NOTICE([summary of build options: Install prefix: ${prefix} APXS: ${APXS} HTTPD-VERSION: ${HTTPD_VERSION} + HTTPD: ${HTTPD} C compiler: ${CC} ${COMPILER_VERSION} CFLAGS: ${CFLAGS} WARNCFLAGS: ${WERROR_CFLAGS} diff --git a/test/modules/tls/conftest.py b/test/modules/tls/conftest.py index 6f6f983..8c4e603 100644 --- a/test/modules/tls/conftest.py +++ b/test/modules/tls/conftest.py @@ -8,9 +8,8 @@ from .env import TlsTestEnv -def pytest_report_header(config, startdir): +def pytest_report_header(config): _x = config - _x = startdir env = TlsTestEnv() return "mod_tls [apache: {aversion}({prefix})]".format( prefix=env.prefix, diff --git a/test/modules/tls/env.py b/test/modules/tls/env.py index e412e6e..30910f0 100644 --- a/test/modules/tls/env.py +++ b/test/modules/tls/env.py @@ -20,16 +20,8 @@ def __init__(self, env: 'HttpdTestEnv'): super().__init__(env=env) self.add_source_dir(os.path.dirname(inspect.getfile(TlsTestSetup))) self.add_modules(["http2", "cgid", "watchdog", "proxy_http2", "ssl"]) + self.add_local_module("tls", "src/.libs/mod_tls.so") - def make(self): - super().make() - self._add_mod_tls() - - def _add_mod_tls(self): - modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf') - with open(modules_conf, 'a') as fd: - # load our test module which is not installed - fd.write(f"LoadModule tls_module \"{self.env.src_dir}/.libs/mod_tls.so\"\n") class TlsCipher: diff --git a/test/pyhttpd/certs.py b/test/pyhttpd/certs.py index 5519f16..a08d5e6 100644 --- a/test/pyhttpd/certs.py +++ b/test/pyhttpd/certs.py @@ -181,6 +181,14 @@ def issue_cert(self, spec: CertificateSpec, chain: List['Credentials'] = None) - creds.issue_certs(spec.sub_specs, chain=subchain) return creds + def save_cert_pem(self, fpath): + with open(fpath, "wb") as fd: + fd.write(self.cert_pem) + + def save_pkey_pem(self, fpath): + with open(fpath, "wb") as fd: + fd.write(self.pkey_pem) + class CertStore: @@ -282,6 +290,7 @@ def create_root(cls, name: str, store_dir: str, key_type: str = "rsa2048") -> Cr def create_credentials(spec: CertificateSpec, issuer: Credentials, key_type: Any, valid_from: timedelta = timedelta(days=-1), valid_to: timedelta = timedelta(days=89), + serial: Optional[int] = None, ) -> Credentials: """Create a certificate signed by this CA for the given domains. :returns: the certificate and private key PEM file paths @@ -289,15 +298,18 @@ def create_credentials(spec: CertificateSpec, issuer: Credentials, key_type: Any if spec.domains and len(spec.domains): creds = HttpdTestCA._make_server_credentials(name=spec.name, domains=spec.domains, issuer=issuer, valid_from=valid_from, - valid_to=valid_to, key_type=key_type) + valid_to=valid_to, key_type=key_type, + serial=serial) elif spec.client: creds = HttpdTestCA._make_client_credentials(name=spec.name, issuer=issuer, email=spec.email, valid_from=valid_from, - valid_to=valid_to, key_type=key_type) + valid_to=valid_to, key_type=key_type, + serial=serial) elif spec.name: creds = HttpdTestCA._make_ca_credentials(name=spec.name, issuer=issuer, valid_from=valid_from, valid_to=valid_to, - key_type=key_type) + key_type=key_type, + serial=serial) else: raise Exception(f"unrecognized certificate specification: {spec}") return creds @@ -320,7 +332,8 @@ def _make_csr( pkey: Any, issuer_subject: Optional[Credentials], valid_from_delta: timedelta = None, - valid_until_delta: timedelta = None + valid_until_delta: timedelta = None, + serial: Optional[int] = None ): pubkey = pkey.public_key() issuer_subject = issuer_subject if issuer_subject is not None else subject @@ -331,7 +344,8 @@ def _make_csr( valid_until = datetime.now() if valid_until_delta is not None: valid_until += valid_until_delta - + if serial is None: + serial = x509.random_serial_number() return ( x509.CertificateBuilder() .subject_name(subject) @@ -339,7 +353,7 @@ def _make_csr( .public_key(pubkey) .not_valid_before(valid_from) .not_valid_after(valid_until) - .serial_number(x509.random_serial_number()) + .serial_number(serial) .add_extension( x509.SubjectKeyIdentifier.from_public_key(pubkey), critical=False, @@ -374,23 +388,28 @@ def _add_ca_usages(csr: Any) -> Any: @staticmethod def _add_leaf_usages(csr: Any, domains: List[str], issuer: Credentials) -> Any: - return csr.add_extension( + csr = csr.add_extension( x509.BasicConstraints(ca=False, path_length=None), critical=True, - ).add_extension( - x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( - issuer.certificate.extensions.get_extension_for_class( - x509.SubjectKeyIdentifier).value), - critical=False - ).add_extension( + ) + if issuer is not None: + csr = csr.add_extension( + x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier( + issuer.certificate.extensions.get_extension_for_class( + x509.SubjectKeyIdentifier).value), + critical=False + ) + csr = csr.add_extension( x509.SubjectAlternativeName([x509.DNSName(domain) for domain in domains]), critical=True, - ).add_extension( + ) + csr = csr.add_extension( x509.ExtendedKeyUsage([ ExtendedKeyUsageOID.SERVER_AUTH, ]), critical=True ) + return csr @staticmethod def _add_client_usages(csr: Any, issuer: Credentials, rfc82name: str = None) -> Any: @@ -421,6 +440,7 @@ def _make_ca_credentials(name, key_type: Any, issuer: Credentials = None, valid_from: timedelta = timedelta(days=-1), valid_to: timedelta = timedelta(days=89), + serial: Optional[int] = None, ) -> Credentials: pkey = _private_key(key_type=key_type) if issuer is not None: @@ -432,7 +452,8 @@ def _make_ca_credentials(name, key_type: Any, subject = HttpdTestCA._make_x509_name(org_name=name, parent=issuer.subject if issuer else None) csr = HttpdTestCA._make_csr(subject=subject, issuer_subject=issuer_subject, pkey=pkey, - valid_from_delta=valid_from, valid_until_delta=valid_to) + valid_from_delta=valid_from, valid_until_delta=valid_to, + serial=serial) csr = HttpdTestCA._add_ca_usages(csr) cert = csr.sign(private_key=issuer_key, algorithm=hashes.SHA256(), @@ -444,15 +465,23 @@ def _make_server_credentials(name: str, domains: List[str], issuer: Credentials, key_type: Any, valid_from: timedelta = timedelta(days=-1), valid_to: timedelta = timedelta(days=89), + serial: Optional[int] = None, ) -> Credentials: name = name pkey = _private_key(key_type=key_type) - subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer.subject) + if issuer is not None: + issuer_subject = issuer.certificate.subject + issuer_key = issuer.private_key + else: + issuer_subject = None + issuer_key = pkey + subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer_subject) csr = HttpdTestCA._make_csr(subject=subject, - issuer_subject=issuer.certificate.subject, pkey=pkey, - valid_from_delta=valid_from, valid_until_delta=valid_to) + issuer_subject=issuer_subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to, + serial=serial) csr = HttpdTestCA._add_leaf_usages(csr, domains=domains, issuer=issuer) - cert = csr.sign(private_key=issuer.private_key, + cert = csr.sign(private_key=issuer_key, algorithm=hashes.SHA256(), backend=default_backend()) return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) @@ -463,14 +492,22 @@ def _make_client_credentials(name: str, key_type: Any, valid_from: timedelta = timedelta(days=-1), valid_to: timedelta = timedelta(days=89), + serial: Optional[int] = None, ) -> Credentials: pkey = _private_key(key_type=key_type) - subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer.subject) + if issuer is not None: + issuer_subject = issuer.certificate.subject + issuer_key = issuer.private_key + else: + issuer_subject = None + issuer_key = pkey + subject = HttpdTestCA._make_x509_name(common_name=name, parent=issuer_subject) csr = HttpdTestCA._make_csr(subject=subject, - issuer_subject=issuer.certificate.subject, pkey=pkey, - valid_from_delta=valid_from, valid_until_delta=valid_to) + issuer_subject=issuer_subject, pkey=pkey, + valid_from_delta=valid_from, valid_until_delta=valid_to, + serial=serial) csr = HttpdTestCA._add_client_usages(csr, issuer=issuer, rfc82name=email) - cert = csr.sign(private_key=issuer.private_key, + cert = csr.sign(private_key=issuer_key, algorithm=hashes.SHA256(), backend=default_backend()) return Credentials(name=name, cert=cert, pkey=pkey, issuer=issuer) diff --git a/test/pyhttpd/conf/httpd.conf.template b/test/pyhttpd/conf/httpd.conf.template index 92cef2e..ef01334 100644 --- a/test/pyhttpd/conf/httpd.conf.template +++ b/test/pyhttpd/conf/httpd.conf.template @@ -2,7 +2,7 @@ ServerName localhost ServerRoot "${server_dir}" DefaultRuntimeDir logs -PidFile httpd.pid +PidFile "${server_dir}/logs/httpd.pid" Include "conf/modules.conf" diff --git a/test/pyhttpd/conf/stop.conf.template b/test/pyhttpd/conf/stop.conf.template index 403bc11..39c674a 100644 --- a/test/pyhttpd/conf/stop.conf.template +++ b/test/pyhttpd/conf/stop.conf.template @@ -6,7 +6,7 @@ ServerName localhost ServerRoot "${server_dir}" DefaultRuntimeDir logs -PidFile httpd.pid +PidFile "${server_dir}/logs/httpd.pid" Include "conf/modules.conf" diff --git a/test/pyhttpd/config.ini.in b/test/pyhttpd/config.ini.in index 1598b4d..85c0cfa 100644 --- a/test/pyhttpd/config.ini.in +++ b/test/pyhttpd/config.ini.in @@ -12,17 +12,18 @@ libexecdir = @libexecdir@ apr_bindir = @APR_BINDIR@ apxs = @bindir@/apxs -apachectl = @sbindir@/apachectl +httpd = @HTTPD@ [httpd] version = @HTTPD_VERSION@ name = @progname@ dso_modules = @DSO_MODULES@ -mpm_modules = @MPM_MODULES@ +static_modules = @STATIC_MODULES@ +mpm_modules = mpm_event mpm_worker [test] +src_dir = @abs_top_srcdir@ gen_dir = @abs_srcdir@/../gen -src_dir = @abs_srcdir@/../../src http_port = 5002 https_port = 5001 proxy_port = 5003 diff --git a/test/pyhttpd/env.py b/test/pyhttpd/env.py index 32129e5..aacedb7 100644 --- a/test/pyhttpd/env.py +++ b/test/pyhttpd/env.py @@ -10,7 +10,7 @@ import time from datetime import datetime, timedelta from string import Template -from typing import List, Optional +from typing import List, Optional, Tuple from configparser import ConfigParser, ExtendedInterpolation from urllib.parse import urlparse @@ -72,6 +72,7 @@ def __init__(self, env: 'HttpdTestEnv'): self._source_dirs = [os.path.dirname(inspect.getfile(HttpdTestSetup))] self._modules = HttpdTestSetup.MODULES.copy() self._optional_modules = [] + self._local_modules = [] def add_source_dir(self, source_dir): self._source_dirs.append(source_dir) @@ -82,6 +83,9 @@ def add_modules(self, modules: List[str]): def add_optional_modules(self, modules: List[str]): self._optional_modules.extend(modules) + def add_local_module(self, mod_name: str, mod_path: str): + self._local_modules.append((mod_name, mod_path)) + def make(self): self._make_dirs() self._make_conf() @@ -92,6 +96,7 @@ def make(self): self.add_modules([self.env.ssl_module]) self._make_modules_conf() self._make_htdocs() + self._add_local_modules() self._add_aptest() self._build_clients() self.env.clear_curl_headerfiles() @@ -197,6 +202,14 @@ def _add_aptest(self): # load our test module which is not installed fd.write(f"LoadModule aptest_module \"{local_dir}/mod_aptest/.libs/mod_aptest.so\"\n") + def _add_local_modules(self): + if len(self._local_modules): + modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf') + with open(modules_conf, 'a') as fd: + for mod_name, mod_path in self._local_modules: + mod_path = os.path.join(self.env.src_dir, mod_path) + fd.write(f"LoadModule {mod_name}_module \"{mod_path}\"\n") + def _build_clients(self): clients_dir = os.path.join( os.path.dirname(os.path.dirname(inspect.getfile(HttpdTestSetup))), @@ -229,6 +242,13 @@ def has_python_package(cls, name: str) -> bool: def get_ssl_module(cls): return os.environ['SSL'] if 'SSL' in os.environ else 'mod_ssl' + @classmethod + def has_shared_module(cls, name): + if cls.LIBEXEC_DIR is None: + env = HttpdTestEnv() # will initialized it + path = os.path.join(cls.LIBEXEC_DIR, f"mod_{name}.so") + return os.path.isfile(path) + def __init__(self, pytestconfig=None): self._our_dir = os.path.dirname(inspect.getfile(Dummy)) self.config = ConfigParser(interpolation=ExtendedInterpolation()) @@ -237,7 +257,7 @@ def __init__(self, pytestconfig=None): self._bin_dir = self.config.get('global', 'bindir') self._apxs = self.config.get('global', 'apxs') self._prefix = self.config.get('global', 'prefix') - self._apachectl = self.config.get('global', 'apachectl') + self._httpd = self.config.get('global', 'httpd') if HttpdTestEnv.LIBEXEC_DIR is None: HttpdTestEnv.LIBEXEC_DIR = self._libexec_dir = self.get_apxs_var('LIBEXECDIR') self._curl = self.config.get('global', 'curl_bin') @@ -256,9 +276,9 @@ def __init__(self, pytestconfig=None): self._proxy_port = int(self.config.get('test', 'proxy_port')) self._ws_port = int(self.config.get('test', 'ws_port')) self._http_tld = self.config.get('test', 'http_tld') - self._src_dir = self.config.get('test', 'src_dir') self._test_dir = self.config.get('test', 'test_dir') self._clients_dir = os.path.join(os.path.dirname(self._test_dir), 'clients') + self._src_dir = self.config.get('test', 'src_dir') self._gen_dir = self.config.get('test', 'gen_dir') self._server_dir = os.path.join(self._gen_dir, 'apache') self._server_conf_dir = os.path.join(self._server_dir, "conf") @@ -266,7 +286,7 @@ def __init__(self, pytestconfig=None): self._server_logs_dir = os.path.join(self.server_dir, "logs") self._server_access_log = os.path.join(self._server_logs_dir, "access_log") self._error_log = HttpdErrorLog(os.path.join(self._server_logs_dir, "error_log")) - self._apachectl_stderr = None + self._httpd_cmd_stderr = None self._dso_modules = self.config.get('httpd', 'dso_modules').split(' ') self._mpm_modules = self.config.get('httpd', 'mpm_modules').split(' ') @@ -467,7 +487,7 @@ def set_current_test_name(self, val) -> None: @property def apachectl_stderr(self): - return self._apachectl_stderr + return self._httpd_cmd_stderr def add_cert_specs(self, specs: List[CertificateSpec]): self._cert_specs.extend(specs) @@ -558,14 +578,17 @@ def mkpath(self, path): if not os.path.exists(path): return os.makedirs(path) - def run(self, args, stdout_list=False, intext=None, inbytes=None, debug_log=True): + def run(self, args, stdout_list=False, intext=None, inbytes=None, debug_log=True, + run_env=None): + if not run_env: + run_env = os.environ if debug_log: log.debug(f"run: {args}") start = datetime.now() if intext is not None: inbytes = intext.encode() p = subprocess.run(args, stderr=subprocess.PIPE, stdout=subprocess.PIPE, - input=inbytes) + input=inbytes, env=run_env) stdout_as_list = None if stdout_list: try: @@ -577,29 +600,27 @@ def run(self, args, stdout_list=False, intext=None, inbytes=None, debug_log=True p.stdout.replace(HttpdTestSetup.CURL_STDOUT_SEPARATOR.encode(), b'') except: pass - r = ExecResult(args=args, exit_code=p.returncode, - stdout=p.stdout, stderr=p.stderr, - stdout_as_list=stdout_as_list, - duration=datetime.now() - start) - return r + return ExecResult(args=args, exit_code=p.returncode, + stdout=p.stdout, stderr=p.stderr, + stdout_as_list=stdout_as_list, + duration=datetime.now() - start) def mkurl(self, scheme, hostname, path='/'): port = self.https_port if scheme == 'https' else self.http_port return f"{scheme}://{hostname}.{self.http_tld}:{port}{path}" def install_test_conf(self, lines: List[str]): - self.apache_stop() with open(self._test_conf, 'w') as fd: fd.write('\n'.join(self._httpd_base_conf)) fd.write('\n') fd.write(f"CoreDumpDirectory {self._server_dir}\n") fd.write('\n') if self._verbosity >= 3: - fd.write(f"LogLevel trace7\n") + fd.write(f"LogLevel trace7 ssl:trace6\n") fd.write(f"DumpIoOutput on\n") fd.write(f"DumpIoInput on\n") elif self._verbosity >= 2: - fd.write(f"LogLevel debug core:trace5 {self.mpm_module}:trace5 http:trace5\n") + fd.write(f"LogLevel debug core:trace5 {self.mpm_module}:trace5 ssl:trace5 http:trace5\n") elif self._verbosity >= 1: fd.write(f"LogLevel info\n") else: @@ -620,7 +641,7 @@ def is_live(self, url: str = None, timeout: timedelta = None): while datetime.now() < try_until: # noinspection PyBroadException try: - r = self.curl_get(url, insecure=True) + r = self.curl_get(url, insecure=True, options=['-vvvv']) if r.exit_code == 0: return True time.sleep(.1) @@ -660,49 +681,68 @@ def is_dead(self, url: str = None, timeout: timedelta = None): log.debug(f"Server still responding after {timeout}") return False - def _run_apachectl(self, cmd) -> ExecResult: + def _httpd_cmd(self, cmd) -> ExecResult: conf_file = 'stop.conf' if cmd == 'stop' else 'httpd.conf' - args = [self._apachectl, + env = os.environ.copy() + args = [self._httpd, "-d", self.server_dir, "-f", os.path.join(self._server_dir, f'conf/{conf_file}'), "-k", cmd] - r = self.run(args) - self._apachectl_stderr = r.stderr + r = self.run(args, run_env=env) + self._httpd_cmd_stderr = r.stderr if r.exit_code != 0: log.warning(f"failed: {r}") return r def apache_reload(self): - r = self._run_apachectl("graceful") + r = self._httpd_cmd("graceful") if r.exit_code == 0: timeout = timedelta(seconds=10) - return 0 if self.is_live(self._http_base, timeout=timeout) else -1 + if self.is_live(self._http_base, timeout=timeout): + return 0 + log.error('failed to reload apache') + self._error_log.dump(log) + return -1 return r.exit_code def apache_restart(self): - self.apache_stop() - r = self._run_apachectl("start") + x = self.apache_stop() + if x != 0: + return x + r = self._httpd_cmd("start") if r.exit_code == 0: timeout = timedelta(seconds=10) - return 0 if self.is_live(self._http_base, timeout=timeout) else -1 + if self.is_live(self._http_base, timeout=timeout): + return 0 + log.error('failed to reload apache') + self._error_log.dump(log) + return -1 return r.exit_code def apache_stop(self): - r = self._run_apachectl("stop") + r = self._httpd_cmd("stop") if r.exit_code == 0: timeout = timedelta(seconds=10) - return 0 if self.is_dead(self._http_base, timeout=timeout) else -1 + if self.is_dead(self._http_base, timeout=timeout): + return 0 + log.error('failed to stop apache') + self._error_log.dump(log) + return -1 return r def apache_graceful_stop(self): log.debug("stop apache") - self._run_apachectl("graceful-stop") - return 0 if self.is_dead() else -1 + self._httpd_cmd("graceful-stop") + if self.is_dead(): + return 0 + log.error('failed to gracefully stop apache') + self._error_log.dump(log) + return -1 def apache_fail(self): log.debug("expect apache fail") - self._run_apachectl("stop") - rv = self._run_apachectl("start") + self._httpd_cmd("stop") + rv = self._httpd_cmd("start") if rv == 0: rv = 0 if self.is_dead() else -1 else: diff --git a/test/pyhttpd/log.py b/test/pyhttpd/log.py index 17b0502..b3aeeff 100644 --- a/test/pyhttpd/log.py +++ b/test/pyhttpd/log.py @@ -150,3 +150,7 @@ def scan_recent(self, pattern: re.Pattern, timeout=10): raise TimeoutError(f"pattern not found in error log after {timeout} seconds") time.sleep(.1) return False + + def dump(self, logger): + for line in open(self.path).readlines(): + logger.error(f'httpd: {line}') diff --git a/test/requirements.txt b/test/requirements.txt new file mode 100644 index 0000000..92e69fe --- /dev/null +++ b/test/requirements.txt @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Copyright 2025 Stefan Eissing (https://dev-icing.de) +# +# Licensed 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. +# +pytest +cryptography +filelock +python-multipart +psutil +tqdm