1
0
mirror of https://github.com/apache/httpd.git synced 2026-10-23 22:49:01 +03:00

Based on [PR665] install what is needed to build Apache httpd with vcpkg in window and fix some tests.

Done with the help of Claude Code Opus 4.6

this closes #731

M    test/modules/http1/env.py
M    test/modules/http2/env.py
M    test/modules/http2/test_006_assets.py
M    test/modules/http2/test_103_upgrade.py
M    test/modules/http2/test_104_padding.py
M    test/modules/http2/test_107_frame_lengths.py
M    test/modules/http2/test_202_trailer.py
M    test/modules/http2/test_400_push.py
M    test/modules/http2/test_401_early_hints.py
M    test/modules/http2/test_710_load_post_static.py
M    test/modules/http2/test_711_load_post_cgi.py
M    test/pyhttpd/certs.py
M    test/pyhttpd/conf/httpd.conf.template
M    test/pyhttpd/env.py


git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1937863 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jean-Frederic Clere
2026-09-04 12:54:05 +00:00
parent 9fb58f1b67
commit 6f1494b474
17 changed files with 397 additions and 68 deletions

View File

@@ -1047,6 +1047,42 @@ IF(OPENSSL_FOUND)
TARGET_LINK_LIBRARIES(abs ${EXTRA_LIBS} ${APR_LIBRARIES} ${OPENSSL_LIBRARIES} Ws2_32.lib)
ENDIF()
IF(OPENSSL_FOUND AND CURL_FOUND AND JANSSON_FOUND)
SET(a2md_md_lib_sources
modules/md/md_acme.c modules/md/md_acme_acct.c
modules/md/md_acme_authz.c modules/md/md_acme_drive.c
modules/md/md_acmev2_drive.c modules/md/md_event.c
modules/md/md_acme_order.c modules/md/md_core.c
modules/md/md_curl.c modules/md/md_crypt.c
modules/md/md_http.c modules/md/md_json.c
modules/md/md_jws.c modules/md/md_log.c
modules/md/md_result.c modules/md/md_reg.c
modules/md/md_status.c modules/md/md_store.c
modules/md/md_store_fs.c modules/md/md_time.c
modules/md/md_ocsp.c modules/md/md_util.c
)
SET(a2md_cmd_sources
support/a2md/md_cmd_main.c
support/a2md/md_cmd_acme.c
support/a2md/md_cmd_reg.c
support/a2md/md_cmd_store.c
)
ADD_EXECUTABLE(a2md ${a2md_cmd_sources} ${a2md_md_lib_sources} build/win32/httpd.rc)
SET(install_targets ${install_targets} a2md)
SET(install_bin_pdb ${install_bin_pdb} $<TARGET_PDB_FILE:a2md>)
SET(tmp_includes ${HTTPD_INCLUDE_DIRECTORIES}
${OPENSSL_INCLUDE_DIR} ${CURL_INCLUDE_DIR} ${JANSSON_INCLUDE_DIR}
${CMAKE_SOURCE_DIR}/modules/md ${CMAKE_SOURCE_DIR}/support/a2md)
SET_TARGET_PROPERTIES(a2md PROPERTIES INCLUDE_DIRECTORIES "${tmp_includes}")
TARGET_COMPILE_DEFINITIONS(a2md PRIVATE
"APP_FILE"
"LONG_NAME=Apache HTTP Server a2md program"
"BIN_NAME=a2md.exe"
)
TARGET_LINK_LIBRARIES(a2md ${EXTRA_LIBS} ${APR_LIBRARIES}
${OPENSSL_LIBRARIES} ${CURL_LIBRARIES} ${JANSSON_LIBRARIES})
ENDIF()
# Unit Test Suite
IF(CHECK_FOUND)
# Get all of the test cases.
@@ -1161,22 +1197,45 @@ ENDIF()
INSTALL(DIRECTORY DESTINATION logs)
INSTALL(DIRECTORY DESTINATION cgi-bin)
INSTALL(CODE "EXECUTE_PROCESS(COMMAND perl \"${CMAKE_CURRENT_SOURCE_DIR}/build/cpR_noreplace.pl\" \"${CMAKE_CURRENT_SOURCE_DIR}/docs/error\" \"${CMAKE_INSTALL_PREFIX}/error\" ifdestmissing)")
INSTALL(CODE "EXECUTE_PROCESS(COMMAND perl \"${CMAKE_CURRENT_SOURCE_DIR}/build/cpR_noreplace.pl\" \"${CMAKE_CURRENT_SOURCE_DIR}/docs/docroot\" \"${CMAKE_INSTALL_PREFIX}/htdocs\" ifdestmissing)")
INSTALL(CODE "EXECUTE_PROCESS(COMMAND perl \"${CMAKE_CURRENT_SOURCE_DIR}/build/cpR_noreplace.pl\" \"${CMAKE_CURRENT_SOURCE_DIR}/docs/icons\" \"${CMAKE_INSTALL_PREFIX}/icons\" ifdestmissing)")
# Copy error, htdocs, icons only if the destination dir doesn't exist yet
# (same semantics as cpR_noreplace.pl ifdestmissing, without Perl)
INSTALL(CODE "
if(NOT IS_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/error\")
file(INSTALL \"${CMAKE_CURRENT_SOURCE_DIR}/docs/error/\" DESTINATION \"${CMAKE_INSTALL_PREFIX}/error\")
endif()
")
INSTALL(CODE "
if(NOT IS_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/htdocs\")
file(INSTALL \"${CMAKE_CURRENT_SOURCE_DIR}/docs/docroot/\" DESTINATION \"${CMAKE_INSTALL_PREFIX}/htdocs\")
endif()
")
INSTALL(CODE "
if(NOT IS_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/icons\")
file(INSTALL \"${CMAKE_CURRENT_SOURCE_DIR}/docs/icons/\" DESTINATION \"${CMAKE_INSTALL_PREFIX}/icons\")
endif()
")
# Copy generated .conf files from the build directory to the install,
# without overwriting stuff already there.
INSTALL(CODE "EXECUTE_PROCESS(COMMAND perl \"${CMAKE_CURRENT_SOURCE_DIR}/build/cpR_noreplace.pl\" \"${CMAKE_BINARY_DIR}/conf\" \"${CMAKE_INSTALL_PREFIX}/conf\")")
# without overwriting stuff already there (no Perl dependency).
INSTALL(CODE "
file(GLOB_RECURSE _conf_files \"${CMAKE_BINARY_DIR}/conf/*\")
foreach(_f \${_conf_files})
# Skip conf/original/ — handled separately below
string(FIND \"\${_f}\" \"/conf/original/\" _is_original)
if(NOT \${_is_original} EQUAL -1)
continue()
endif()
file(RELATIVE_PATH _rel \"${CMAKE_BINARY_DIR}/conf\" \"\${_f}\")
set(_dest \"${CMAKE_INSTALL_PREFIX}/conf/\${_rel}\")
if(NOT EXISTS \"\${_dest}\")
get_filename_component(_dest_dir \"\${_dest}\" DIRECTORY)
file(COPY \"\${_f}\" DESTINATION \"\${_dest_dir}\")
endif()
endforeach()
")
# But conf/original is supposed to be overwritten.
# Note: FILE(TO_NATIVE_PATH ...) leaves the backslashes unescaped, which
# generates warnings. Just do it manually since this build only supports
# Windows anyway.
STRING(REPLACE "/" "\\\\" native_src ${CMAKE_BINARY_DIR}/conf/original)
STRING(REPLACE "/" "\\\\" native_dest ${CMAKE_INSTALL_PREFIX}/conf/original)
INSTALL(CODE "EXECUTE_PROCESS(COMMAND xcopy \"${native_src}\" \"${native_dest}\" /Q /S /Y)")
INSTALL(DIRECTORY "${CMAKE_BINARY_DIR}/conf/original/"
DESTINATION conf/original)
STRING(TOUPPER "${CMAKE_BUILD_TYPE}" buildtype)
MESSAGE(STATUS "")

View File

@@ -1,5 +1,7 @@
import sys
import os
import re
import subprocess
import warnings
import pytest
@@ -8,6 +10,11 @@ sys.path.append(os.path.join(os.path.dirname(__file__), '.'))
from pyhttpd.env import HttpdTestEnv
def pytest_ignore_collect(collection_path, config):
if "pytest_suite" in collection_path.parts:
return True
def pytest_report_header(config, start_path):
env = HttpdTestEnv()
return f"[apache httpd: {env.get_httpd_version()}, mpm: {env.mpm_module}, {env.prefix}]"
@@ -19,6 +26,115 @@ def pytest_addoption(parser):
parser.addoption("--archive", action="store", default=None,
help='Archive the server dir after each test package '
'to the specified folder')
parser.addoption("--httpd", action="store", default=None,
help="path to httpd binary (used to derive prefix, version, etc.)")
parser.addoption("--conf", action="store", default=None,
help="path to the installed httpd.conf (for LoadModule discovery "
"when apxs is not available, e.g. on Windows)")
def _generate_config_ini(config):
"""Generate a config.ini from --httpd/--conf when no config.ini exists."""
httpd_opt = config.getoption("--httpd")
conf_opt = config.getoption("--conf")
if not httpd_opt:
return
httpd_path = os.path.abspath(httpd_opt)
prefix = os.path.dirname(os.path.dirname(httpd_path))
bindir = os.path.join(prefix, "bin")
libexecdir = os.path.join(prefix, "modules")
is_win = sys.platform == "win32"
# version from httpd -v
version = "2.5.0"
try:
p = subprocess.run([httpd_path, "-v"], capture_output=True, text=True)
m = re.search(r"Apache/(\S+)", p.stdout)
if m:
version = m.group(1)
except Exception:
pass
# modules and mpm from conf file
dso_modules = []
mpm_modules = []
if conf_opt:
conf_path = os.path.abspath(conf_opt)
try:
with open(conf_path) as f:
for line in f:
line = line.strip()
if line.startswith("#"):
continue
m = re.match(r"LoadModule\s+(\S+)_module", line)
if m:
name = m.group(1)
if name.startswith("mpm_"):
mpm_modules.append(name)
else:
dso_modules.append(name)
except Exception:
pass
if not mpm_modules:
mpm_modules = ["mpm_winnt"] if is_win else ["mpm_event"]
if is_win and "cgid" in dso_modules and "cgi" not in dso_modules:
dso_modules = ["cgi" if m == "cgid" else m for m in dso_modules]
apachectl = httpd_path if is_win else os.path.join(bindir, "apachectl")
if is_win:
prefix_curl = os.path.join(prefix, "bin", "curl.exe")
curl_bin = prefix_curl if os.path.isfile(prefix_curl) else "curl.exe"
else:
curl_bin = "curl"
test_dir = os.path.dirname(os.path.abspath(__file__))
gen_dir = os.path.join(test_dir, "gen")
ini_content = f"""[global]
prefix = {prefix}
exec_prefix = ${{prefix}}
bindir = ${{prefix}}/bin
sbindir = ${{exec_prefix}}/sbin
libdir = ${{exec_prefix}}/lib
libexecdir = ${{exec_prefix}}/modules
apr_bindir = ${{prefix}}
apxs = ${{prefix}}/bin/apxs
apachectl = {apachectl}
curl_bin = {curl_bin}
nghttp = nghttp
h2load = h2load
pre_built_test_binaries_dir = ${{libexecdir}}
[httpd]
version = {version}
name = httpd
dso_modules = {' '.join(dso_modules)}
mpm_modules = {' '.join(mpm_modules)}
[test]
gen_dir = {gen_dir}
http_port = 5002
https_port = 5001
proxy_port = 5003
http_port2 = 5004
ws_port = 5100
http_tld = tests.httpd.apache.org
test_dir = {test_dir}
test_src_dir = {test_dir}
"""
ini_path = os.path.join(test_dir, "pyhttpd", "config.ini")
with open(ini_path, "w") as f:
f.write(ini_content)
def pytest_configure(config):
httpd_opt = config.getoption("--httpd", default=None)
if httpd_opt and sys.platform == "win32":
_generate_config_ini(config)
def pytest_generate_tests(metafunc):
@@ -52,6 +168,15 @@ def _module_scope(env):
def _package_scope(env, request):
env.httpd_error_log.clear_ignored_matches()
env.httpd_error_log.clear_ignored_lognos()
if sys.platform == "win32":
env.httpd_error_log.add_ignored_lognos([
"AH00098",
"AH10373",
"AH00336",
"AH00337",
"AH00338",
"AH00341",
])
yield
assert env.apache_stop() == 0
env.check_error_log()

View File

@@ -30,7 +30,7 @@ class AAATestEnv(HttpdTestEnv):
def __init__(self, pytestconfig=None):
super().__init__(pytestconfig=pytestconfig)
self.add_httpd_log_modules(["auth_digest", "authn_file", "authz_core"])
self._digest_pwfile = os.path.join(self.server_dir, "digest.passwd")
self._digest_pwfile = f"{self.server_dir}/digest.passwd"
def setup_httpd(self, setup: HttpdTestSetup = None):
super().setup_httpd(setup=AAATestSetup(env=self))

View File

@@ -24,19 +24,24 @@ class H1TestSetup(HttpdTestSetup):
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}")
module_dir = self.env.test_modules_dir
if not self.env.isWindows:
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}")
module_dir = f"{local_dir}/mod_h1test/.libs"
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")
fd.write(f"LoadModule h1test_module \"{module_dir}/mod_h1test.so\"\n")
def _setup_data_1k_1m(self):
s90 = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n"

View File

@@ -25,19 +25,24 @@ class H2TestSetup(HttpdTestSetup):
self._setup_data_1k_1m()
def _add_h2test(self):
local_dir = os.path.dirname(inspect.getfile(H2TestSetup))
p = subprocess.run([self.env.apxs, '-c', 'mod_h2test.c'],
capture_output=True,
cwd=os.path.join(local_dir, 'mod_h2test'))
rv = p.returncode
if rv != 0:
log.error(f"compiling md_h2test failed: {p.stderr}")
raise Exception(f"compiling md_h2test failed: {p.stderr}")
module_dir = self.env.test_modules_dir
if not self.env.isWindows:
local_dir = os.path.dirname(inspect.getfile(H2TestSetup))
p = subprocess.run([self.env.apxs, '-c', 'mod_h2test.c'],
capture_output=True,
cwd=os.path.join(local_dir, 'mod_h2test'))
rv = p.returncode
if rv != 0:
log.error(f"compiling md_h2test failed: {p.stderr}")
raise Exception(f"compiling md_h2test failed: {p.stderr}")
module_dir = f"{local_dir}/mod_h2test/.libs"
modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf')
with open(modules_conf, 'a') as fd:
# load our test module which is not installed
fd.write(f"LoadModule h2test_module \"{local_dir}/mod_h2test/.libs/mod_h2test.so\"\n")
fd.write(f"LoadModule h2test_module \"{module_dir}/mod_h2test.so\"\n")
def _setup_data_1k_1m(self):
s90 = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678\n"

View File

@@ -4,6 +4,7 @@ from .env import H2Conf, H2TestEnv
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestAssets:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -4,6 +4,7 @@ from .env import H2Conf, H2TestEnv
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestUpgrade:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -9,6 +9,7 @@ def frame_padding(payload, padbits):
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestPadding:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -16,6 +16,7 @@ def mk_text_file(fpath: str, lines: int):
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestFrameLengths:
URI_PATHS = []

View File

@@ -14,6 +14,7 @@ def setup_data(env):
# The trailer tests depend on "nghttp" as no other client seems to be able to send those
# rare things.
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestTrailers:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -6,6 +6,7 @@ from .env import H2Conf, H2TestEnv
# The push tests depend on "nghttp"
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestPush:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -5,6 +5,7 @@ from .env import H2Conf, H2TestEnv
# The push tests depend on "nghttp"
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_nghttp(), reason="nghttp not available")
class TestEarlyHints:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -5,6 +5,7 @@ from .env import H2Conf, H2TestEnv
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_h2load(), reason="h2load not available")
class TestLoadPostStatic:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -5,6 +5,7 @@ from .env import H2Conf, H2TestEnv
@pytest.mark.skipif(condition=H2TestEnv.is_unsupported(), reason="mod_http2 not supported here")
@pytest.mark.skipif(not H2TestEnv().has_h2load(), reason="h2load not available")
class TestLoadCgi:
@pytest.fixture(autouse=True, scope='class')

View File

@@ -174,7 +174,7 @@ class Credentials:
if spec.sub_specs:
if self._store:
sub_store = CertStore(fpath=os.path.join(self._store.path, creds.name))
sub_store = CertStore(fpath=f"{self._store.path}/{creds.name}")
creds.set_store(sub_store)
subchain = chain.copy() if chain else []
subchain.append(self)
@@ -231,7 +231,7 @@ class CertStore:
chain.append(creds)
if not with_root and len(chain) > 1:
chain = chain[:-1]
chain_file = os.path.join(self._store_dir, f'{name}-{infix}.pem')
chain_file = f"{self._store_dir}/{name}-{infix}.pem"
with open(chain_file, "wb") as fd:
for c in chain:
fd.write(c.cert_pem)
@@ -246,11 +246,11 @@ class CertStore:
def get_cert_file(self, name: str, key_type=None) -> str:
key_infix = ".{0}".format(key_type) if key_type is not None else ""
return os.path.join(self._store_dir, f'{name}{key_infix}.cert.pem')
return f"{self._store_dir}/{name}{key_infix}.cert.pem"
def get_pkey_file(self, name: str, key_type=None) -> str:
key_infix = ".{0}".format(key_type) if key_type is not None else ""
return os.path.join(self._store_dir, f'{name}{key_infix}.pkey.pem')
return f"{self._store_dir}/{name}{key_infix}.pkey.pem"
def load_pem_cert(self, fpath: str) -> x509.Certificate:
with open(fpath) as fd:

View File

@@ -60,4 +60,10 @@ RequestReadTimeout header=10 body=10
Options +ExecCGI
</Directory>
<IfModule mpm_winnt_module>
<IfModule mod_cgi.c>
ScriptInterpreterSource Script
</IfModule>
</IfModule>

View File

@@ -3,11 +3,13 @@ import inspect
import logging
import re
import os
import signal
import shutil
import stat
import subprocess
import sys
import time
import platform
from datetime import datetime, timedelta
from string import Template
from typing import List, Optional
@@ -92,6 +94,7 @@ class HttpdTestSetup:
self.add_modules([self.env.ssl_module])
self._make_modules_conf()
self._make_htdocs()
self._add_aptest()
self._build_clients()
self.env.clear_curl_headerfiles()
@@ -101,12 +104,13 @@ class HttpdTestSetup:
os.makedirs(self.env.gen_dir)
# wipe logs from any previous module run to avoid stale files in the archive
if os.path.isdir(self.env.server_logs_dir):
shutil.rmtree(self.env.server_logs_dir)
os.makedirs(self.env.server_logs_dir)
shutil.rmtree(self.env.server_logs_dir,
ignore_errors=(sys.platform == "win32"))
os.makedirs(self.env.server_logs_dir, exist_ok=True)
def _make_conf(self):
# remove anything from another run/test suite
conf_dest_dir = os.path.join(self.env.server_dir, 'conf')
conf_dest_dir = f"{self.env.server_dir}/conf"
if os.path.isdir(conf_dest_dir):
shutil.rmtree(conf_dest_dir)
for d in self._source_dirs:
@@ -148,13 +152,16 @@ class HttpdTestSetup:
with open(modules_conf, 'w') as fd:
# issue load directives for all modules we want that are shared
missing_mods = list()
for m in self._modules:
modules = self._modules
if sys.platform == "win32":
modules = ["cgi" if m == "cgid" else m for m in modules]
for m in modules:
match = re.match(r'^mod_(.+)$', m)
if match:
m = match.group(1)
if m in loaded:
continue
mod_path = os.path.join(self.env.libexec_dir, f"mod_{m}.so")
mod_path = f"{self.env.libexec_dir}/mod_{m}.so"
if m in builtin_modules:
fd.write(f"#built-in: LoadModule {m}_module \"{mod_path}\"\n")
elif os.path.isfile(mod_path):
@@ -170,7 +177,7 @@ class HttpdTestSetup:
m = match.group(1)
if m in loaded:
continue
mod_path = os.path.join(self.env.libexec_dir, f"mod_{m}.so")
mod_path = f"{self.env.libexec_dir}/mod_{m}.so"
if m not in builtin_modules and os.path.isfile(mod_path):
fd.write(f"LoadModule {m}_module \"{mod_path}\"\n")
loaded.add(m)
@@ -181,7 +188,7 @@ class HttpdTestSetup:
def _make_htdocs(self):
if not os.path.exists(self.env.server_docs_dir):
os.makedirs(self.env.server_docs_dir)
dest_dir = os.path.join(self.env.server_dir, 'htdocs')
dest_dir = f"{self.env.server_dir}/htdocs"
# remove anything from another run/test suite
if os.path.isdir(dest_dir):
shutil.rmtree(dest_dir)
@@ -189,30 +196,49 @@ class HttpdTestSetup:
srcdocs = os.path.join(d, 'htdocs')
if os.path.isdir(srcdocs):
shutil.copytree(srcdocs, dest_dir, dirs_exist_ok=True)
# make all contained .py scripts executable
# make all contained .py scripts executable and fix shebangs on Windows
for dirpath, _dirnames, filenames in os.walk(dest_dir):
for fname in filenames:
if re.match(r'.+\.py', fname):
py_file = os.path.join(dirpath, fname)
if sys.platform == "win32":
self._fix_shebang(py_file)
st = os.stat(py_file)
os.chmod(py_file, st.st_mode | stat.S_IEXEC)
@staticmethod
def _fix_shebang(py_file):
python_path = sys.executable
python_path = python_path.replace("\\", "/")
with open(py_file, 'r') as f:
lines = f.readlines()
if lines and re.match(r'#!\s*/usr/bin/env\s+python', lines[0]):
lines[0] = f"#!{python_path}\n"
with open(py_file, 'w', newline='\n') as f:
f.writelines(lines)
def _add_aptest(self):
local_dir = os.path.dirname(inspect.getfile(HttpdTestSetup))
p = subprocess.run([self.env.apxs, '-c', 'mod_aptest.c'],
capture_output=True,
cwd=os.path.join(local_dir, 'mod_aptest'))
rv = p.returncode
if rv != 0:
log.error(f"compiling mod_aptest failed: {p.stderr}")
raise Exception(f"compiling mod_aptest failed: {p.stderr}")
module_dir = self.env.test_modules_dir
if not self.env.isWindows:
local_dir = os.path.dirname(inspect.getfile(HttpdTestSetup))
p = subprocess.run([self.env.apxs, '-c', 'mod_aptest.c'],
capture_output=True,
cwd=os.path.join(local_dir, 'mod_aptest'))
rv = p.returncode
if rv != 0:
log.error(f"compiling mod_aptest failed: {p.stderr}")
raise Exception(f"compiling mod_aptest failed: {p.stderr}")
module_dir = f"{local_dir}/mod_aptest/.libs/"
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 aptest_module \"{local_dir}/mod_aptest/.libs/mod_aptest.so\"\n")
# load our test module
fd.write(f"LoadModule aptest_module \"{module_dir}/mod_aptest.so\"\n")
def _build_clients(self):
if self.env.test_modules_dir:
return
clients_dir = os.path.join(
os.path.dirname(os.path.dirname(inspect.getfile(HttpdTestSetup))),
'clients')
@@ -269,8 +295,15 @@ class HttpdTestEnv:
self._apxs = self.config.get('global', 'apxs')
self._prefix = self.config.get('global', 'prefix')
self._apachectl = self.config.get('global', 'apachectl')
self._libexec_dir = self.config.get('global', 'libexecdir')
HttpdTestEnv.LIBEXEC_DIR = self._libexec_dir
if HttpdTestEnv.LIBEXEC_DIR is None:
HttpdTestEnv.LIBEXEC_DIR = self._libexec_dir = self.get_apxs_var('LIBEXECDIR')
self._httpd_ver = self.config.get('httpd', 'version')
self._curl = self.config.get('global', 'curl_bin')
if 'CURL' in os.environ:
self._curl = os.environ['CURL']
@@ -280,6 +313,8 @@ class HttpdTestEnv:
self._h2load = self.config.get('global', 'h2load')
if self._h2load is None:
self._h2load = 'h2load'
self._test_modules_dir = self.config.get('global', 'pre_built_test_binaries_dir',
fallback=None)
self._http_port = int(self.config.get('test', 'http_port'))
self._http_port2 = int(self.config.get('test', 'http_port2'))
@@ -288,19 +323,29 @@ class HttpdTestEnv:
self._ws_port = int(self.config.get('test', 'ws_port'))
self._http_tld = self.config.get('test', 'http_tld')
self._test_dir = self.config.get('test', 'test_dir')
# In cmake determining the test directory is a headache and this is a simple workaround
if not self._test_dir:
self._test_dir = os.getcwd() + "/pyhttpd" if "pyhttpd" not in os.getcwd() else os.getcwd()
self._clients_dir = os.path.join(os.path.dirname(self._test_dir), 'clients')
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")
self._server_docs_dir = os.path.join(self._server_dir, "htdocs")
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._server_dir = self._server_dir.replace("\\","/")
self._server_conf_dir = f"{self._server_dir}/conf"
self._server_docs_dir = f"{self._server_dir}/htdocs"
self._server_logs_dir = f"{self._server_dir}/logs"
self._server_run_dir = f"{self._server_dir}/run"
self._server_access_log = f"{self._server_logs_dir}/access_log"
error_log_sep = "." if self.isWindows else "_"
self._error_log = HttpdErrorLog(f"{self._server_logs_dir}/error{error_log_sep}log")
self._apachectl_stderr = None
self._httpd_proc = None
self._dso_modules = self.config.get('httpd', 'dso_modules').split(' ')
self._mpm_modules = self.config.get('httpd', 'mpm_modules').split(' ')
self._mpm_module = f"mpm_{os.environ['MPM']}" if 'MPM' in os.environ else 'mpm_event'
self._mpm_module = f"mpm_{os.environ['MPM']}" if 'MPM' in os.environ else 'mpm_winnt' if self.isWindows else 'mpm_event'
self._ssl_module = self.get_ssl_module()
if len(self._ssl_module.strip()) == 0:
self._ssl_module = None
@@ -310,7 +355,7 @@ class HttpdTestEnv:
self._https_base = f"https://{self._httpd_addr}:{self.https_port}"
self._verbosity = pytestconfig.option.verbose if pytestconfig is not None else 0
self._test_conf = os.path.join(self._server_conf_dir, "test.conf")
self._test_conf = f"{self._server_conf_dir}/test.conf"
self._httpd_base_conf = []
self._httpd_env = {}
self._httpd_log_modules = ['aptest']
@@ -353,7 +398,7 @@ class HttpdTestEnv:
def issue_certs(self):
if self._ca is None:
self._ca = HttpdTestCA.create_root(name=self.http_tld,
store_dir=os.path.join(self.server_dir, 'ca'),
store_dir=f"{self.server_dir}/ca",
key_type="rsa4096")
self._ca.issue_certs(self._cert_specs)
@@ -538,6 +583,10 @@ class HttpdTestEnv:
def server_logs_dir(self) -> str:
return self._server_logs_dir
@property
def server_run_dir(self) -> str:
return self._server_run_dir
@property
def libexec_dir(self) -> str:
return HttpdTestEnv.LIBEXEC_DIR
@@ -581,9 +630,17 @@ class HttpdTestEnv:
self._current_test = val
@property
def apachectl_stderr(self):
def apachectl_stderr(self) -> str:
return self._apachectl_stderr
@property
def test_modules_dir(self) -> str:
return self._test_modules_dir
@property
def isWindows(self) -> bool:
return platform.system().lower() == "windows"
def add_cert_specs(self, specs: List[CertificateSpec]):
self._cert_specs.extend(specs)
@@ -656,7 +713,11 @@ class HttpdTestEnv:
return False
def has_nghttp(self):
return self._nghttp != ""
if self._nghttp == "":
return False
if os.path.dirname(self._nghttp):
return os.path.isfile(self._nghttp) and os.access(self._nghttp, os.X_OK)
return self.has_tool(self._nghttp)
def has_nghttp_get_assets(self):
if not self.has_nghttp():
@@ -675,7 +736,7 @@ class HttpdTestEnv:
return p.stdout.strip()
def get_httpd_version(self) -> str:
return self.get_apxs_var("HTTPD_VERSION")
return self._httpd_ver
def mkpath(self, path):
if not os.path.exists(path):
@@ -803,7 +864,7 @@ class HttpdTestEnv:
conf_file = 'stop.conf' if cmd == 'stop' else 'httpd.conf'
args = [self._apachectl,
"-d", self.server_dir,
"-f", os.path.join(self._server_dir, f'conf/{conf_file}'),
"-f", f"{self._server_dir}/conf/{conf_file}",
"-k", cmd]
r = self.run(args, env=self._clean_path_env())
self._apachectl_stderr = r.stderr
@@ -811,7 +872,51 @@ class HttpdTestEnv:
log.warning(f"failed: {r}")
return r
def _win_httpd_args(self):
httpd = os.path.join(self._bin_dir, 'httpd')
return [httpd,
"-d", self.server_dir,
"-f", f"{self.server_dir}/conf/httpd.conf",
"-DFOREGROUND"]
def _win_start(self) -> int:
self._win_stop()
args = self._win_httpd_args()
log.debug(f"starting httpd: {args}")
self._httpd_proc = subprocess.Popen(args, env=self._clean_path_env())
time.sleep(1)
rc = self._httpd_proc.poll()
if rc is not None:
log.error(f"httpd exited immediately with code {rc}")
self._httpd_proc = None
return -1
timeout = timedelta(seconds=30)
if self.is_live(self._http_base, timeout=timeout):
return 0
log.error(f"httpd not responding at {self._http_base}")
rc = self._httpd_proc.poll()
if rc is not None:
log.error(f"httpd process died with code {rc}")
return -1
def _win_stop(self) -> int:
if self._httpd_proc is not None:
log.debug("stopping httpd (terminate parent)")
self._httpd_proc.terminate()
try:
self._httpd_proc.wait(timeout=10)
except subprocess.TimeoutExpired:
self._httpd_proc.kill()
self._httpd_proc.wait(timeout=5)
self._httpd_proc = None
if not self.is_dead(self._http_base, timeout=timedelta(seconds=60)):
log.warning("port still in use after stop")
return 0
def apache_reload(self):
if self.isWindows:
self._win_stop()
return self._win_start()
r = self._run_apachectl("graceful")
if r.exit_code == 0:
timeout = timedelta(seconds=10)
@@ -819,14 +924,21 @@ class HttpdTestEnv:
return r.exit_code
def apache_restart(self):
if self.isWindows:
self._win_stop()
return self._win_start()
self.apache_stop()
r = self._run_apachectl("start")
if r.exit_code == 0:
timeout = timedelta(seconds=10)
return 0 if self.is_live(self._http_base, timeout=timeout) else -1
return r.exit_code
def apache_stop(self):
if self.isWindows:
self._win_stop()
timeout = timedelta(seconds=10)
return 0 if self.is_dead(self._http_base, timeout=timeout) else -1
r = self._run_apachectl("stop")
if r.exit_code == 0:
timeout = timedelta(seconds=10)
@@ -835,11 +947,18 @@ class HttpdTestEnv:
def apache_graceful_stop(self):
log.debug("stop apache")
if self.isWindows:
self._win_stop()
return 0 if self.is_dead() else -1
self._run_apachectl("graceful-stop")
return 0 if self.is_dead() else -1
def apache_fail(self):
log.debug("expect apache fail")
if self.isWindows:
self._win_stop()
rv = self._win_start()
return 0 if rv != 0 else (0 if self.is_dead() else -1)
self._run_apachectl("stop")
rv = self._run_apachectl("start")
if rv == 0:
@@ -1029,7 +1148,8 @@ class HttpdTestEnv:
def curl_protocol_version(self, url, timeout=5, options=None):
if not options:
options = []
options.extend(["-w", "%{http_version}\n", "-o", "/dev/null"])
devnull = "NUL" if sys.platform == "win32" else "/dev/null"
options.extend(["-w", "%{http_version}\n", "-o", devnull])
r = self.curl_raw(url, timeout=timeout, options=options)
if r.exit_code == 0 and r.response:
return r.response["body"].decode('utf-8').rstrip()