mirror of
https://github.com/apache/httpd.git
synced 2026-10-23 22:49:01 +03:00
CMake: Build mod_proxy_uwsgi; enable mod_mime_magic by default:
* CMakeLists.txt: Add mod_proxy_uwsgi, linked against mod_proxy. Change mod_mime_magic from optional to included by default. * test/modules/proxy/test_03_uwsgi.py: Remove duplicate copy of the module appended after the first. (_proxy_uwsgi_setup): Add LogLevel trace4. * test/modules/proxy/uwsgi_faker.py: Remove duplicate copy of the module appended after the first. Submitted by: Paul Lodge <paullodge outlook.com> GitHub: closes #781 git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1938757 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
"""
|
||||
Tests for mod_proxy_uwsgi fix.
|
||||
|
||||
@@ -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()
|
||||
@@ -66,94 +66,7 @@ class TestProxyUwsgi:
|
||||
"""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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user