]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
mod_proxy: add response handling tests
authorGiannis Christodoulou <ichristod@apache.org>
Thu, 4 Jun 2026 06:55:13 +0000 (06:55 +0000)
committerGiannis Christodoulou <ichristod@apache.org>
Thu, 4 Jun 2026 06:55:13 +0000 (06:55 +0000)
Add coverage for backend Content-Type handling, X-Forwarded-*
headers and request body preservation during balancer failover.

Add a simple TCP backend faker used by the tests.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1934963 13f79535-47bb-0310-9956-ffa450edef68

test/modules/proxy/env.py
test/modules/proxy/test_03_response.py [new file with mode: 0644]

index 098d4d4948519495c4d1df020360597866fff528..92e85ba9fc456e6fa0d2dec5bde1571ee8a3b82b 100644 (file)
@@ -1,22 +1,72 @@
 import inspect
 import logging
 import os
-import subprocess
-from typing import Dict, Any
+import socket
+from threading import Thread
 
 from pyhttpd.certs import CertificateSpec
-from pyhttpd.conf import HttpdConf
+
 from pyhttpd.env import HttpdTestEnv, HttpdTestSetup
 
 log = logging.getLogger(__name__)
 
 
+class TCPFaker:
+    # tcp backend for custom responses
+
+    def __init__(self, host, port):
+        self._thread = None
+        self._socket = None
+        self._host = host
+        self._port = port
+        self._done = False
+
+    def start(self):
+        def process():
+            self._socket.listen(1)
+            self._socket.settimeout(0.5)
+            self._process()
+
+        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self._socket.bind((self._host, self._port))
+        self._thread = Thread(target=process, daemon=True)
+        self._thread.start()
+
+    def stop(self):
+        self._done = True
+        self._thread.join(timeout=5)
+        self._socket.close()
+
+    def _make_response(self, data):
+        return """HTTP/1.1 200 OK
+Content-Type: text/html
+Content-Length: 5
+
+Hello""".encode()
+
+    def _process(self):
+        while not self._done:
+            try:
+                c, client_address = self._socket.accept()
+                try:
+                    data = c.recv(4096)
+                    c.sendall(self._make_response(data))
+                finally:
+                    c.close()
+            except socket.timeout:
+                pass
+            except ConnectionAbortedError:
+                self._done = True
+
+
 class ProxyTestSetup(HttpdTestSetup):
 
     def __init__(self, env: 'HttpdTestEnv'):
         super().__init__(env=env)
         self.add_source_dir(os.path.dirname(inspect.getfile(ProxyTestSetup)))
-        self.add_modules(["proxy", "proxy_http", "proxy_balancer", "lbmethod_byrequests"])
+        self.add_modules(["proxy", "proxy_http", "proxy_ajp", "proxy_balancer",
+                          "lbmethod_byrequests", "remoteip"])
 
 
 class ProxyTestEnv(HttpdTestEnv):
@@ -24,17 +74,20 @@ class ProxyTestEnv(HttpdTestEnv):
     def __init__(self, pytestconfig=None):
         super().__init__(pytestconfig=pytestconfig)
         self.add_httpd_conf([
-                         ])
+        ])
         self._d_reverse = f"reverse.{self.http_tld}"
         self._d_forward = f"forward.{self.http_tld}"
         self._d_mixed = f"mixed.{self.http_tld}"
 
-        self.add_httpd_log_modules(["proxy", "proxy_http", "proxy_balancer", "lbmethod_byrequests", "ssl"])
+        self.add_httpd_log_modules(
+            ["proxy", "proxy_http", "proxy_balancer", "lbmethod_byrequests",
+             "ssl"])
         self.add_cert_specs([
             CertificateSpec(domains=[
                 self._d_forward, self._d_reverse, self._d_mixed
             ]),
-            CertificateSpec(domains=[f"noh2.{self.http_tld}"], key_type='rsa2048'),
+            CertificateSpec(domains=[f"noh2.{self.http_tld}"],
+                            key_type='rsa2048'),
         ])
 
     def setup_httpd(self, setup: HttpdTestSetup = None):
diff --git a/test/modules/proxy/test_03_response.py b/test/modules/proxy/test_03_response.py
new file mode 100644 (file)
index 0000000..60a4afa
--- /dev/null
@@ -0,0 +1,124 @@
+import re
+
+import pytest
+
+from pyhttpd.conf import HttpdConf
+from .env import TCPFaker
+
+
+class _ResponseFaker(TCPFaker):
+
+    def _make_response(self, data):
+        first_line = data.split(b"\r\n")[0].decode("latin-1")
+        path = first_line.split(" ")[1] if " " in first_line else "/"
+
+        if "/content-type" in path:
+            return """HTTP/1.1 200 OK\r\nContent-Type: 
+            text/html\x00extra\r\n\r\n""".encode()
+
+        if "/forwarded" in path:
+            headers = data.split(b"\r\n\r\n")[0].decode("latin-1")
+            forwarded = []
+            for line in headers.split("\r\n"):
+                if line.lower().startswith("x-forwarded-"):
+                    forwarded.append(line)
+            return ("HTTP/1.1 200 OK\r\n" + "\r\n".join(
+                forwarded) + "\r\n\r\n").encode()
+
+        if "/balancer" in path:
+            headers = data.split(b"\r\n\r\n")[0].decode("latin-1")
+            body_lines = []
+            for line in headers.split("\r\n"):
+                if ": " in line:
+                    name, value = line.split(": ", 1)
+                    body_lines.append(f"{name.upper()} = {value}")
+            body = "\r\n".join(body_lines)
+            return (
+                    "HTTP/1.1 200 OK\r\n"
+                    "Content-Type: text/plain\r\n"
+                    f"Content-Length: {len(body.encode())}\r\n"
+                    "\r\n"
+                    + body
+            ).encode()
+
+        return super()._make_response(data)
+
+
+class TestProxyResponse:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        faker = _ResponseFaker("127.0.0.1", env.http_port2)
+        faker.start()
+
+        conf = HttpdConf(env)
+        conf.add([
+            '<Proxy "balancer://xqacluster">',
+            f'    BalancerMember "http://127.0.0.1:{env.http_port2 + 1}"',
+            f'    BalancerMember "http://127.0.0.1:{env.http_port2}"',
+            '    ProxySet forcerecovery=Off',
+            '</Proxy>',
+        ])
+        conf.start_vhost(domains=[f"test1.{env.http_tld}"], port=env.http_port,
+                         doc_root="htdocs", with_ssl=False)
+        conf.add([
+            "ProxyPass /balancer balancer://xqacluster/balancer",
+            f"ProxyPass / http://127.0.0.1:{env.http_port2}/",
+            f"ProxyPassReverse / http://127.0.0.1:{env.http_port2}/",
+        ])
+        if env.has_shared_module("remoteip"):
+            conf.add([
+                "RemoteIPHeader X-Forwarded-For",
+                "RemoteIPTrustedProxy 0.0.0.0",
+            ])
+        conf.end_vhost()
+        conf.install()
+        assert env.apache_restart() == 0
+        yield
+        faker.stop()
+
+    # check Content-Type
+    def test_proxy_03_001(self, env):
+        if not env.httpd_is_at_least("2.4.55"):
+            pytest.skip(f'need at least httpd 2.4.55 for this')
+
+        r = env.curl_get(env.mkurl("http", "test1", "/content-type"))
+        assert r.response["status"] == 502
+
+        env.httpd_error_log.ignore_recent(
+            lognos=["AH01106", "AH10404"]
+        )
+
+    # checks X-Forwarded headers
+    def test_proxy_03_002(self, env):
+        if not env.httpd_is_at_least("2.4.54"):
+            pytest.skip(f'need at least httpd 2.4.54 for this')
+
+        r = env.curl_get(env.mkurl("http", "test1", "/forwarded"), options=[
+            '-H', 'Connection: close, X-Forwarded-For, X-Forwarded-Host, '
+                  'X-Forwarded-Server',
+        ])
+        assert r.response["status"] == 200
+        assert "x-forwarded-for" in r.response["header"]
+        assert "x-forwarded-host" in r.response["header"]
+        assert "x-forwarded-server" in r.response["header"]
+
+    # check body is preserved after failover
+    def test_proxy_03_003(self, env):
+        if not env.httpd_is_at_least("2.4.42"):
+            pytest.skip(f'need at least httpd 2.4.42 for this')
+
+        post_body = "123testing"
+        r = env.curl_get(
+            env.mkurl("http", "test1", "/balancer"),
+            options=['--data', post_body, '-H', 'Content-Type: text/plain; '
+                                                'charset=utf-8']
+        )
+        assert r.response["status"] == 200
+        body = r.response["body"].decode("latin-1")
+        m = re.search(r'CONTENT-LENGTH = (\d+)', body, re.IGNORECASE)
+        assert m is not None
+        assert int(m.group(1)) == len(post_body)
+        env.httpd_error_log.ignore_recent(
+            lognos=["AH00957", "AH00959", "AH01114"]
+        )