Skip to content
Closed
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
4 changes: 3 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ SET(MODULE_LIST
"modules/metadata/mod_expires+I+Expires header control"
"modules/metadata/mod_headers+A+HTTP header control"
"modules/metadata/mod_ident+O+RFC 1413 identity check"
"modules/metadata/mod_mime_magic+O+automagically determining MIME type"
"modules/metadata/mod_mime_magic+I+automagically determining MIME type"
"modules/metadata/mod_remoteip+I+translate header contents to an apparent client remote_ip"
"modules/metadata/mod_setenvif+A+basing ENV vars on headers"
"modules/metadata/mod_unique_id+I+per-request unique ids"
Expand All @@ -410,6 +410,7 @@ SET(MODULE_LIST
"modules/proxy/mod_proxy_hcheck+I+Apache proxy Health check module. Requires and is enabled by --enable-proxy."
"modules/proxy/mod_proxy_http+I+Apache proxy HTTP module. Requires and is enabled by --enable-proxy."
"modules/proxy/mod_proxy_scgi+I+Apache proxy SCGI module. Requires and is enabled by --enable-proxy."
"modules/proxy/mod_proxy_uwsgi+I+Apache proxy SCGI module. Requires and is enabled by --enable-proxy."
"modules/proxy/mod_proxy_wstunnel+I+Apache proxy Websocket Tunnel module. Requires and is enabled by --enable-proxy."
"modules/http2/mod_proxy_http2+i+Apache proxy HTTP/2 module. Requires --enable-proxy."
"modules/proxy/mod_serf+O+Reverse proxy module using Serf"
Expand Down Expand Up @@ -609,6 +610,7 @@ IF(LIBXML2_FOUND)
SET(mod_proxy_html_extra_libs "${LIBXML2_LIBRARIES};${LIBXML2_ICONV_LIBRARIES}")
ENDIF()
SET(mod_proxy_scgi_extra_libs mod_proxy)
SET(mod_proxy_uwsgi_extra_libs mod_proxy)
SET(mod_proxy_wstunnel_extra_libs mod_proxy)
SET(mod_lbmethod_bybusyness_extra_libs mod_proxy)
SET(mod_lbmethod_bytraffic_extra_libs mod_proxy)
Expand Down
91 changes: 2 additions & 89 deletions test/modules/proxy/test_03_uwsgi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

"""
Tests for mod_proxy_uwsgi fix.

Expand Down Expand Up @@ -31,6 +30,7 @@ def _proxy_uwsgi_setup(env):
conf.add([
"LogLevel proxy_uwsgi:debug",
f"ProxyPass / uwsgi://127.0.0.1:{UWSGI_PORT}/",
"LogLevel trace4",
])
conf.end_vhost()
conf.install()
Expand Down Expand Up @@ -66,94 +66,7 @@ def test_proxy_03_003(self, env):
"""Transfer-Encoding is stripped by mod_proxy_uwsgi."""
r = env.curl_get(
f"http://{env.d_reverse}:{env.http_port}/?te=1",
5,
)

print("response =", r.response)
print("stderr =", getattr(r, "stderr", None))
print("stdout =", getattr(r, "stdout", None))
print("exitcode =", getattr(r, "exit_code", None))
print("json =", getattr(r, "json", None))

assert r.response is not None
assert r.response["status"] == 200
assert "transfer-encoding" not in r.response["header"]

assert r.json["host"] == "uwsgi-faker"

assert env.httpd_error_log.scan_recent(
re.compile(
r".*uwsgi: removing hop-by-hop header 'Transfer-Encoding'"
)
)
"""
Tests for mod_proxy_uwsgi fix.

Per PEP 3333, WSGI applications must not generate hop-by-hop
headers. Remove any such headers received from the backend.
"""

import re

import pytest

from pyhttpd.conf import HttpdConf
from .uwsgi_faker import UwsgiFaker


UWSGI_PORT = 5200


@pytest.fixture(autouse=True, scope="class")
def _proxy_uwsgi_setup(env):
faker = UwsgiFaker(port=UWSGI_PORT)
faker.start()

conf = HttpdConf(env)
conf.start_vhost(
domains=[env.d_reverse],
port=env.http_port,
with_ssl=False,
)
conf.add([
"LogLevel proxy_uwsgi:debug",
f"ProxyPass / uwsgi://127.0.0.1:{UWSGI_PORT}/",
])
conf.end_vhost()
conf.install()

assert env.apache_restart() == 0

yield

faker.stop()


class TestProxyUwsgi:

def test_proxy_03_001(self, env):
"""uWSGI backend response is forwarded correctly."""
r = env.curl_get(
f"http://{env.d_reverse}:{env.http_port}/",
5,
)
assert r.response["status"] == 200
assert r.json["host"] == "uwsgi-faker"

def test_proxy_03_002(self, env):
"""No Transfer-Encoding header reaches the client."""
r = env.curl_get(
f"http://{env.d_reverse}:{env.http_port}/",
5,
)
assert r.response["status"] == 200
assert "transfer-encoding" not in r.response["header"]

def test_proxy_03_003(self, env):
"""Transfer-Encoding is stripped by mod_proxy_uwsgi."""
r = env.curl_get(
f"http://{env.d_reverse}:{env.http_port}/?te=1",
5,
5,
)

print("response =", r.response)
Expand Down
146 changes: 0 additions & 146 deletions test/modules/proxy/uwsgi_faker.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,152 +112,6 @@ def _handle(conn):
conn.close()


class UwsgiFaker:
"""Fake uWSGI backend running in a daemon thread."""

def __init__(self, port: int):
self._port = port
self._done = False
self._sock = None
self._thread = None

def start(self):
self._sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self._sock.bind(("127.0.0.1", self._port))
self._sock.listen(16)
self._thread = threading.Thread(target=self._serve, daemon=True)
self._thread.start()

def stop(self):
self._done = True
try:
self._sock.close()
except OSError:
pass

def _serve(self):
while not self._done:
try:
conn, _ = self._sock.accept()
except OSError:
break
t = threading.Thread(target=_handle, args=(conn,), daemon=True)
t.start()
"""
Minimal fake uWSGI backend for testing mod_proxy_uwsgi.

Listens on a TCP port and speaks the uWSGI wire protocol. mod_proxy_uwsgi
sends a binary uwsgi packet (4-byte header + CGI key/value pairs) followed by
the optional request body, and expects a plain HTTP/1.x response back.

The QUERY_STRING CGI variable controls what the response looks like:

QUERY_STRING="" - normal 200 response, no Transfer-Encoding
QUERY_STRING="te=1" - 200 response with Transfer-Encoding: chunked injected
(simulates a buggy Python app; mod_proxy_uwsgi must
strip it before forwarding to the client)
"""
import socket
import struct
import threading


BODY = b'{"host": "uwsgi-faker"}'


def _parse_uwsgi_vars(data):
"""Parse the CGI key/value payload from a uwsgi packet.

Packet layout (from the uWSGI protocol spec):
[modifier1: u8][payload_len: u16le][modifier2: u8][payload: bytes]
Payload is a sequence of:
[keylen: u16le][key: bytes][vallen: u16le][val: bytes]

Returns a dict of the decoded variables, or {} on parse error.
"""
if len(data) < 4:
return {}
payload_len = struct.unpack_from("<H", data, 1)[0]
if len(data) < 4 + payload_len:
return {}
pos = 4
end = 4 + payload_len
vars_ = {}
while pos < end:
if pos + 2 > end:
break
klen = struct.unpack_from("<H", data, pos)[0]
pos += 2
if pos + klen > end:
break
key = data[pos:pos + klen].decode("latin-1")
pos += klen
if pos + 2 > end:
break
vlen = struct.unpack_from("<H", data, pos)[0]
pos += 2
if pos + vlen > end:
break
val = data[pos:pos + vlen].decode("latin-1")
pos += vlen
vars_[key] = val
return vars_


def _recv_uwsgi_request(conn):
"""Read a complete uwsgi request packet from the connection.

Returns the raw bytes of the complete packet (header + payload), or b""
on connection close.
"""
# Read the 4-byte uwsgi header
header = b""
while len(header) < 4:
chunk = conn.recv(4 - len(header))
if not chunk:
return b""
header += chunk

payload_len = struct.unpack_from("<H", header, 1)[0]

payload = b""
while len(payload) < payload_len:
chunk = conn.recv(payload_len - len(payload))
if not chunk:
return b""
payload += chunk

return header + payload


def _handle(conn):
try:
packet = _recv_uwsgi_request(conn)
if not packet:
return

cgi_vars = _parse_uwsgi_vars(packet)
qs = cgi_vars.get("QUERY_STRING", "")
inject_te = "te=1" in qs.split("&")

headers = [
b"HTTP/1.1 200 OK",
b"Server: UwsgiFaker",
b"Content-Type: application/json",
]
if inject_te:
# Inject the invalid header that mod_proxy_uwsgi must strip
headers.append(b"Transfer-Encoding: chunked")
headers.append(b"Content-Length: " + str(len(BODY)).encode())
headers.append(b"")
headers.append(b"")

conn.sendall(b"\r\n".join(headers) + BODY)
finally:
conn.close()


class UwsgiFaker:
"""Fake uWSGI backend running in a daemon thread."""

Expand Down