Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions test/modules/core/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ 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","include","userdir","suexec","headers"])
self.add_modules(["cgid","include","userdir","suexec","headers",
"actions","asis","info","status","mime",
"negotiation"])


class CoreTestEnv(HttpdTestEnv):

def __init__(self, pytestconfig=None):
super().__init__(pytestconfig=pytestconfig)
self.add_httpd_log_modules(["http", "core"])
self.add_httpd_log_modules(["http", "core", "info", "status"])

def setup_httpd(self, setup: HttpdTestSetup = None):
super().setup_httpd(setup=CoreTestSetup(env=self))
1 change: 1 addition & 0 deletions test/modules/core/htdocs/cgi/env_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
data = {
"REQUEST_METHOD": os.getenv("REQUEST_METHOD", ""),
"QUERY_STRING": os.getenv("QUERY_STRING", ""),
"HTTP_PROXY": os.getenv("HTTP_PROXY", ""),
}

print(json.dumps(data, indent=2))
1 change: 1 addition & 0 deletions test/modules/core/htdocs/test1/actionstest/dummy.chtml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a dummy file for testing mod_actions.
4 changes: 4 additions & 0 deletions test/modules/core/htdocs/test1/asistest/example.ahtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Status: 301 Moved
Location: http://example.com/

This has moved.
5 changes: 5 additions & 0 deletions test/modules/core/htdocs/test1/cgi/handler.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python3

print("Content-Type: text/plain")
print()
print("this file was processed")
16 changes: 11 additions & 5 deletions test/modules/core/test_003_cgi_env_vars.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ def _class_scope(self, env):
assert env.apache_restart() == 0

def test_cgi_003_01(self, env):
"""
CVE-2025-65082:
Configuration-defined env vars must not override
server-calculated CGI env vars.
"""
"""Configuration-defined env vars must not override
server-calculated CGI env vars."""
url = env.mkurl("http", "cgi", "/env_parameters.py?x=123")
r = env.curl_get(url)
assert r.response["status"] == 200
assert r.response["json"]["REQUEST_METHOD"] == "GET"
assert r.response["json"]["QUERY_STRING"] == "x=123"

def test_cgi_003_02(self, env):
"""A client-supplied Proxy header must not be propagated as
HTTP_PROXY to CGI scripts (httpoxy)."""
url = env.mkurl("http", "cgi", "/env_parameters.py")
r = env.curl_get(url, options=['-H', 'Proxy: http://evil.example.com'])
assert r.response["status"] == 200
assert r.response["json"]["HTTP_PROXY"] == "", \
"HTTP_PROXY should be empty — Proxy header must not leak to CGI"
131 changes: 131 additions & 0 deletions test/modules/core/test_010_handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import os
import pytest

from pyhttpd.conf import HttpdConf


class TestHandlers:

@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
doc_dir = os.path.join(env.server_dir, "htdocs", "test1")
conf = HttpdConf(env, extras={
'base': f"""
<Location /our-server-info>
SetHandler server-info
Require all granted
</Location>

<Location /our-server-status>
SetHandler server-status
Require all granted
</Location>
""",
f"test1.{env.http_tld}": f"""
<Directory "{doc_dir}/cgi">
Options +ExecCGI
AddHandler cgi-script .py
</Directory>
Action html-cgi /cgi/handler.py
AddHandler html-cgi .chtml
AddHandler send-as-is .ahtml
""",
})
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

# mod_info: verify server-info handler returns server information page
def test_core_010_01(self, env):
url = env.mkurl("http", "test1", "/our-server-info")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 200
body = r.response["body"].decode()
assert "Apache Server Information" in body

# mod_info: verify ?list query returns module list
def test_core_010_02(self, env):
url = env.mkurl("http", "test1", "/our-server-info?list")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 200
body = r.response["body"].decode()
assert "Server Module List" in body

# mod_status: verify basic server-status page
def test_core_010_03(self, env):
url = env.mkurl("http", "test1", "/our-server-status")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 200
body = r.response["body"].decode()
assert "Apache Server Status" in body

# mod_status: verify extended status shows worker information
def test_core_010_04(self, env):
# Reconfigure with ExtendedStatus On
conf = HttpdConf(env, extras={
'base': f"""
ExtendedStatus On

<Location /our-server-status>
SetHandler server-status
Require all granted
</Location>
""",
})
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

url = env.mkurl("http", "test1", "/our-server-status")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 200
body = r.response["body"].decode()
assert "requests currently being processed" in body or \
"idle workers" in body

# mod_actions: verify Action directive routes request through CGI handler
def test_core_010_05(self, env):
# Re-apply the full config (010_04 changed it)
doc_dir = os.path.join(env.server_dir, "htdocs", "test1")
conf = HttpdConf(env, extras={
'base': f"""
<Location /our-server-info>
SetHandler server-info
Require all granted
</Location>
<Location /our-server-status>
SetHandler server-status
Require all granted
</Location>
""",
f"test1.{env.http_tld}": f"""
<Directory "{doc_dir}/cgi">
Options +ExecCGI
AddHandler cgi-script .py
</Directory>
Action html-cgi /cgi/handler.py
AddHandler html-cgi .chtml
AddHandler send-as-is .ahtml
""",
})
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

url = env.mkurl("http", "test1", "/actionstest/dummy.chtml")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 200
body = r.response["body"].decode()
assert "this file was processed" in body

# mod_asis: verify send-as-is handler sends raw response
def test_core_010_06(self, env):
url = env.mkurl("http", "test1", "/asistest/example.ahtml")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 301
42 changes: 42 additions & 0 deletions test/modules/core/test_011_userdir.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os
import pytest

from pyhttpd.conf import HttpdConf


class TestUserDir:

@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
# Create userdir content that will be served for any ~user request.
# Using UserDir with an absolute path makes every /~user/path resolve
# to {absolute_path}/path, avoiding the need for real system users.
userdir_base = os.path.join(env.server_dir, "htdocs", "userdir")
hello_dir = os.path.join(userdir_base, "anyuser", "hello")
os.makedirs(hello_dir, exist_ok=True)
with open(os.path.join(hello_dir, "world.txt"), "w") as f:
f.write("Hello World!")

conf = HttpdConf(env, extras={
'base': f"""
UserDir "{userdir_base}"

<Directory "{userdir_base}">
AllowOverride None
Require all granted
</Directory>
"""
})
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

# UserDir with absolute path: /~anyuser/hello/world.txt serves from
# the configured directory regardless of the username
def test_core_011_01(self, env):
url = env.mkurl("http", "test1", "/~anyuser/hello/world.txt")
r = env.curl_get(url)
assert r.response, f"no response: {r.stderr}"
assert r.response["status"] == 200
body = r.response["body"].decode()
assert "Hello World!" in body
85 changes: 85 additions & 0 deletions test/modules/core/test_012_malformed_requests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import os
import re
import pytest

from pyhttpd.conf import HttpdConf

class TestNull:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
conf = HttpdConf(env)
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

def test_core_012_01(self, env):
url = f"https://localhost:{env.http_port}/"
env.curl_raw(url, options=[
'--connect-timeout', '5', '--max-time', '10', '-k',
])

class TestBadRequest:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
conf = HttpdConf(env, extras={
'base': """
KeepAlive On
Redirect 301 /2bad /destination
""",
})
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

def test_core_012_02(self, env):
url = env.mkurl("http", "test1", "/2bad")
r = env.curl_raw(url, options=[
'-H', 'Content-Length: 1', '-v',
'--connect-timeout', '5', '--max-time', '10',
])
if r.response is not None:
assert r.response["status"] != 301, \
"Should not redirect if request body not fully consumed"
assert r.response["status"] in [400, 408], \
f"Expected 400 or 408, got {r.response['status']}"
else:
assert "Closing connection" in r.stderr or \
"Connection reset" in r.stderr or \
r.exit_code != 0, \
"Connection should be closed, not left intact"
assert "HTTP/1.1 301" not in r.stderr, \
"Should not redirect if request body not fully consumed"
env.httpd_error_log.ignore_recent(
lognos=["AH10390"],
matches=[r'.*:error\].*', r'.*:warn\].*'])


class TestBadOptions:
@pytest.fixture(autouse=True, scope='class')
def _class_scope(self, env):
htdocs = os.path.join(env.server_dir, "htdocs", "test1")
ob_dir = os.path.join(htdocs, "badoption")
os.makedirs(ob_dir, exist_ok=True)
with open(os.path.join(ob_dir, ".htaccess"), "w") as f:
f.write("<Limit INVALID XXX FOO BAR BAZ>\n</Limit>\n")
conf = HttpdConf(env, extras={
f"test1.{env.http_tld}": f"""
<Directory "{ob_dir}">
AllowOverride Limit
</Directory>
""",
})
conf.add_vhost_test1()
conf.install()
assert env.apache_restart() == 0

def test_core_012_03(self, env):
"""OPTIONS request to directory with invalid Limit directive should
return 500 and log a configuration error, not leak memory."""
url = env.mkurl("http", "test1", "/badoption/")
r = env.curl_raw(url, options=['-X', 'OPTIONS', '-v'])
assert r.response is not None
assert r.response["status"] == 500
env.httpd_error_log.ignore_recent(
matches=[r'.*Could not register method.*',
r'.*core:error.*'])