diff --git a/CMakeLists.txt b/CMakeLists.txt index 46811e0d87d..d760616f7ea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,8 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.16...3.29) PROJECT(HTTPD C) +set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}../vcpkg/scripts/buildsystems/vcpkg.cmake" CACHE STRING "toolchain") + INCLUDE(CheckSymbolExists) INCLUDE(CheckCSourceCompiles) INCLUDE("build/build-modules-c.cmake") @@ -428,7 +430,13 @@ SET(MODULE_LIST "modules/test/mod_optional_hook_import+O+example optional hook importer" "modules/test/mod_policy+I+HTTP protocol compliance filters" ) - +IF(BUILD_PYHTTPD_MODULES) + LIST(APPEND MODULE_LIST + "test/pyhttpd/mod_aptest/mod_aptest+A+Test module. Useful only for developers and testing purposes." + "test/modules/http1/mod_h1test/mod_h1test+A+Http/1 Test module. Useful only for developers and testing purposes." + "test/modules/http2/mod_h2test/mod_h2test+A+Http/2 Test module. Useful only for developers and testing purposes." + ) +ENDIF() # Track which modules actually built have APIs to link against. SET(installed_mod_libs_exps) @@ -1065,6 +1073,40 @@ INSTALL(TARGETS ${install_targets} INSTALL(TARGETS ${install_modules} RUNTIME DESTINATION modules ) +INSTALL(CODE [[ + SET(SERVER_ROOT "${CMAKE_INSTALL_PREFIX}") + + CONFIGURE_FILE( + "../support/apachectl.bat.in" + "${CMAKE_CURRENT_BINARY_DIR}/support/apachectl.bat" + @ONLY + ) +]]) +INSTALL(FILES "${CMAKE_CURRENT_BINARY_DIR}/support/apachectl.bat" + DESTINATION bin + ) + + INSTALL(CODE [[ + find_package(Git QUIET) + + if(GIT_FOUND) + execute_process( + COMMAND ${GIT_EXECUTABLE} branch --show-current + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE GIT_BRANCH + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_QUIET + ) + endif() + + SET(SERVER_ROOT "${CMAKE_INSTALL_PREFIX}") + + CONFIGURE_FILE( + "../test/pyhttpd/config_win.ini.in" + "../test/pyhttpd/config.ini" + @ONLY + ) +]]) IF(INSTALL_PDB) INSTALL(FILES ${install_bin_pdb} diff --git a/build-win.bat b/build-win.bat new file mode 100644 index 00000000000..baa61b7f7f3 --- /dev/null +++ b/build-win.bat @@ -0,0 +1,107 @@ +@ECHO off +@REM + +@REM ------------------------------------- +@REM Variables used in this script +@REM ------------------------------------- +SET CWD=%CD% +SET "CWD=%CWD:\=/%" +@REM ------------------------------------- +SET ARCH="x64-windows" +SET BUILD_TYPE="Debug" +SET VCPKG_DIRECTORY="vcpkg\installed\x64-windows" +SET VCPKG_DIRECTORY_LIB=%VCPKG_DIRECTORY% +SET GENERATOR="NMake Makefiles" +@REM SET GENERATOR="Ninja" +SET CMAKE_VERSION="4.0" +SET HTTPD_INSTALL_DIRECTORY="C:/Users/%USERNAME%/Projects/http-debug/apache" +@REM ------------------------------------- + +@REM ------------------------------------- +@REM Installs via MS vcpkg manager all the +@REM packages needed to build httpd +@REM ------------------------------------- +if NOT EXIST vcpkg\ ( + echo "Adding...." + git submodule add --force https://github.com/microsoft/vcpkg.git vcpkg + PUSHD vcpkg\ + CALL bootstrap-vcpkg.bat +) else ( + echo "Updating...." + git submodule update --remote --merge vcpkg/ + PUSHD vcpkg\ +) + +SET VCPKG_ROOT=%CWD%\vcpkg +@REM For some reason using the manifest doesn't install the default-features +vcpkg.exe install --triplet x64-windows apr[private-headers] apr-util pcre2 openssl nghttp2 curl libxml2 jansson + +POPD + +@REM ------------------------------------- +@REM Setup build env using VS build 2022 +@REM It maybe installed somewhere else, if so, the path will need updating to find the batch file +@REM ------------------------------------- +call "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvars64.bat" + +@REM ------------------------------------- +@REM Create a temp dir for cmake files +@REM ------------------------------------- +if not EXIST build-asf-win\ ( + mkdir build-asf-win\ +) + +PUSHD build-asf-win\ + +@REM ------------------------------------- +@REM Run cmake so that it configures the +@REM and creates the needed makefiles +@REM ------------------------------------- +cmake .. -B . ^ + -G %GENERATOR% ^ + -DCMAKE_BUILD_TYPE=%BUILD_TYPE% ^ + -DCMAKE_TOOLCHAIN_FILE=%CWD%/vcpkg/scripts/buildsystems/vcpkg.cmake ^ + -DVCPKG_TARGET_TRIPLET=%ARCH% ^ + -DNGHTTP2_INCLUDE_DIR=%CWD%/vcpkg/installed/x64-windows/include ^ + -DAPR_INCLUDE_DIR=%CWD%/vcpkg/installed/x64-windows/include ^ + "-DAPR_LIBRARIES=%CWD%/vcpkg/installed/x64-windows/lib/libapr-1.lib;%CWD%/vcpkg/installed/x64-windows/lib/libaprutil-1.lib" ^ + -DNGHTTP2_LIBRARIES=%CWD%/vcpkg/installed/x64-windows/lib/nghttp2.lib ^ + -DBUILD_PYHTTPD_MODULES=true ^ + -DCMAKE_POLICY_VERSION_MINIMUM=%CMAKE_VERSION% ^ + --install-prefix %HTTPD_INSTALL_DIRECTORY% + +@REM ------------------------------------- +@REM Build httpd +@REM ------------------------------------- +cmake --build . + +@REM ------------------------------------- +@REM Install httpd only +@REM ------------------------------------- +cmake --install . --config %BUILD_TYPE% +POPD + +@REM ------------------------------------- +@REM Copy the dependencies. +@REM ------------------------------------- +if %BUILD_TYPE% == "Debug" ( + SET VCPKG_DIRECTORY_LIB=%VCPKG_DIRECTORY_LIB%\debug +) + +@REM ------------------------------------- +@REM Copy the libraries needed to run httpd +@REM ------------------------------------- +xcopy %VCPKG_DIRECTORY_LIB%\bin\* %HTTPD_INSTALL_DIRECTORY%\bin /y /f /i + +@REM ------------------------------------- +@REM Run the pyHttpd tests +@REM ------------------------------------- +PUSHD test\ +python.exe -m pip install --upgrade pip +python.exe -m venv .venv +call .venv\Scripts\activate +call python.exe -m pip install websockets filelock cryptography pyopenssl python-multipart pebble pytest-html pytest==8.4.2 cffi +call python.exe -m pytest -vvv --ignore="modules/md" --ignore="modules/http2" -k "not test_cgi_003_01 and not test_800_websockets and not test_02_unix and not test_003_get and not test_004_post and not test_002_restarts and not test_005_trailers" --junitxml="reports/pytest-results.xml" --html="reports/Results" --self-contained-html +deactivate + +POPD diff --git a/support/apachectl.bat.in b/support/apachectl.bat.in new file mode 100644 index 00000000000..fddfc14f7d9 --- /dev/null +++ b/support/apachectl.bat.in @@ -0,0 +1,71 @@ +echo off +SETLOCAL EnableDelayedExpansion +set "BIN_INSTALL_DIR=@SERVER_ROOT@/bin" + +:: ----------------------------------------------------------------------------- +:: Apache Control Script for Windows +:: By Paul Lodge +:: March 2026 +:: Version 0.0.1 +:: ----------------------------------------------------------------------------- + +set "APACHE24=Apache2.x" +set "HTTPD=%BIN_INSTALL_DIR%\httpd.exe" +set "ACMD=%~1" + +:: Check if a command was provided +if "%ACMD%"=="" goto DEFAULT + +:: Switch Logic +if /I "%ACMD%"=="install" goto INSTALL +if /I "%ACMD%"=="uninstall" goto UNINSTALL +if /I "%ACMD%"=="start" goto START +if /I "%ACMD%"=="stop" goto STOP +if /I "%ACMD%"=="restart" goto RESTART +if /I "%ACMD%"=="graceful" goto RESTART +if /I "%ACMD%"=="startssl" goto SSL_ERROR +if /I "%ACMD%"=="sslstart" goto SSL_ERROR +if /I "%ACMD%"=="start-SSL" goto SSL_ERROR +if /I "%ACMD%"=="configtest" goto NOT_SUPPORTED +if /I "%ACMD%"=="status" goto NOT_SUPPORTED +if /I "%ACMD%"=="fullstatus" goto NOT_SUPPORTED + +goto DEFAULT + +:INSTALL + "%HTTPD%" -k install + goto END + +:UNINSTALL + "%HTTPD%" -k uninstall -n %APACHE24% + goto END + +:START + net start %APACHE24% + goto END + +:STOP + net stop %APACHE24% + goto END + +:RESTART + net stop %APACHE24% + net start %APACHE24% + goto END + +:SSL_ERROR + echo The startssl option is no longer supported. + echo Please edit httpd.conf to include the SSL configuration settings + echo and then use 'apachectl start'. + goto END + +:NOT_SUPPORTED + echo Not supported! + goto END + +:DEFAULT + "%HTTPD%" %* + goto END + +:END +ENDLOCAL diff --git a/test/modules/core/htdocs/ssi/exec.shtml b/test/modules/core/htdocs/ssi/exec.shtml index e98afb15ddc..a5f987991be 100644 --- a/test/modules/core/htdocs/ssi/exec.shtml +++ b/test/modules/core/htdocs/ssi/exec.shtml @@ -1 +1,2 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/test/modules/core/htdocs/userdir/testuser/public_html/cgi-bin/test.cgi b/test/modules/core/htdocs/userdir/testuser/public_html/cgi-bin/test.cgi index 9c8c67c2093..9b7b795b935 100755 --- a/test/modules/core/htdocs/userdir/testuser/public_html/cgi-bin/test.cgi +++ b/test/modules/core/htdocs/userdir/testuser/public_html/cgi-bin/test.cgi @@ -1,4 +1,3 @@ -#!/bin/sh echo "Content-Type: text/plain" echo echo "OK" diff --git a/test/modules/core/test_002_restarts.py b/test/modules/core/test_002_restarts.py index cf203bc82db..2b4333d08ab 100644 --- a/test/modules/core/test_002_restarts.py +++ b/test/modules/core/test_002_restarts.py @@ -84,12 +84,11 @@ def _rtime(self, s: str) -> timedelta: 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") +@pytest.mark.skipif(condition=CoreTestEnv().isWindows or 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): diff --git a/test/modules/http1/env.py b/test/modules/http1/env.py index e2df1a561f2..1a88e3b032f 100644 --- a/test/modules/http1/env.py +++ b/test/modules/http1/env.py @@ -24,19 +24,24 @@ def make(self): 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" diff --git a/test/modules/http2/env.py b/test/modules/http2/env.py index e2647616aaf..f4e2ca50a99 100644 --- a/test/modules/http2/env.py +++ b/test/modules/http2/env.py @@ -25,19 +25,24 @@ def make(self): 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" diff --git a/test/pyhttpd/env.py b/test/pyhttpd/env.py index 7c2eb33dacb..5079ed1ec0f 100644 --- a/test/pyhttpd/env.py +++ b/test/pyhttpd/env.py @@ -8,6 +8,7 @@ import subprocess import sys import time +import platform from datetime import datetime, timedelta from string import Template from typing import List, Optional @@ -92,6 +93,7 @@ def make(self): 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() @@ -183,21 +185,27 @@ def _make_htdocs(self): os.chmod(py_file, st.st_mode | stat.S_IEXEC) 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') @@ -247,8 +255,15 @@ def __init__(self, pytestconfig=None): 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'] @@ -258,6 +273,8 @@ def __init__(self, pytestconfig=None): 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')) @@ -266,19 +283,27 @@ def __init__(self, pytestconfig=None): 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_run_dir = os.path.join(self.server_dir, "run") 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")) + error_log_sep = "." if self.isWindows else "_" + self._error_log = HttpdErrorLog(os.path.join(self._server_logs_dir, f"error{error_log_sep}log")) self._apachectl_stderr = 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 @@ -427,6 +452,10 @@ def server_dir(self) -> str: 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 @@ -470,9 +499,17 @@ def set_current_test_name(self, val) -> None: 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) @@ -556,7 +593,7 @@ def get_apxs_var(self, name: str) -> str: 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):