| 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 "/empty-header" in path: | |
| body = b"Hello" | |
| return ( | |
| b"HTTP/1.1 200 OK\r\n" | |
| b"X-Empty:\r\n" | |
| b"Content-Length: 5\r\n" | |
| b"\r\n" | |
| + body | |
| ) | |
| 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", | |
| "RemoteIPTrustedProxy 127.0.0.1", | |
| ]) | |
| 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"] | |
| ) | |
| # empty backend response header values are valid | |
| def test_proxy_03_004(self, env): | |
| r = env.curl_get(env.mkurl("http", "test1", "/empty-header")) | |
| assert r.response["status"] == 200 | |
| assert "x-empty" in r.response["header"] | |
| assert r.response["body"] == b"Hello" | |
| # a trailing empty RemoteIPHeader token must not underflow the trim pointer | |
| def test_proxy_03_005(self, env): | |
| env.require(env.has_shared_module("remoteip"), "need mod_remoteip for this", | |
| dep="mod_remoteip", | |
| detected=f"no mod_remoteip.so in {env.libexec_dir}") | |
| r = env.curl_get(env.mkurl("http", "test1", "/forwarded"), options=[ | |
| '-H', 'X-Forwarded-For: 192.0.2.1,', | |
| ]) | |
| assert r.response["status"] == 200 |