test: add REST_HOST_HEADER to send Host override only for direct prosody URLs
Always sending Host: <xmpp domain> to /rest broke when REST was fronted by an ingress (TLS hostname mismatch -> 431). Add a --rest-host-header option (REST_HOST_HEADER env, default 'auto') that sends the XMPP domain Host only for IP/localhost URLs where prosody needs it for vhost routing; 'none' disables, any other value is sent verbatim. Documents the option in tests/README.md. Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
This commit is contained in:
+2
-1
@@ -74,6 +74,7 @@ Tests are configured via **environment variables** or **CLI options**.
|
||||
| `REST_URL` | *(empty)* | `mod_http_rest` URL |
|
||||
| `REST_USER` | *(empty)* | HTTP Basic Auth user for `/rest` (reverse proxy credential) |
|
||||
| `REST_PASSWORD` | *(empty)* | HTTP Basic Auth password for `/rest` (reverse proxy credential) |
|
||||
| `REST_HOST_HEADER` | `auto` | Host header for `/rest`: `auto` sends the XMPP domain only for IP/localhost URLs (direct prosody needs it for vhost routing; ingress hostnames reject a mismatching Host, e.g. 431), `none` disables, anything else is sent verbatim |
|
||||
| `VERIFY_SSL` | *(unset)* | Verify TLS certificates (default: False for self-signed dev certs) |
|
||||
| `MUC_DOMAIN` | *(empty)* | MUC component domain |
|
||||
| `ADMIN_TELNET_HOST` | `127.0.0.1` | Admin telnet host |
|
||||
@@ -378,7 +379,7 @@ and point the container's `hybridaAuthUrl`/`fcm_api_url`/`del_api_url`/
|
||||
- **Two distinct accounts** (`--xmpp-jid` and `--xmpp-jid2`) are strongly recommended. Tests that share the same bare JID between both clients are marked `xfail` rather than silently passing.
|
||||
- MUC tests create temporary rooms and attempt to destroy them after each test.
|
||||
- The REST injection test now performs a **roundtrip**: inject via REST, query MAM, assert the message appears.
|
||||
- If the `/rest` endpoint is protected by a reverse proxy with HTTP Basic Auth, provide `--rest-user` and `--rest-password` (or `REST_USER` / `REST_PASSWORD` env vars). The `RESTInjector` helper sends a `Basic` Authorization header when these are configured. It also sends `Host: <xmpp domain>` because prosody routes HTTP to a vhost by the `Host` header.
|
||||
- If the `/rest` endpoint is protected by a reverse proxy with HTTP Basic Auth, provide `--rest-user` and `--rest-password` (or `REST_USER` / `REST_PASSWORD` env vars). The `RESTInjector` helper sends a `Basic` Authorization header when these are configured. The `Host: <xmpp domain>` override needed for prosody vhost routing is only sent for direct (IP/localhost) REST URLs; ingress-fronted hostnames rewrite Host themselves and reject a mismatching override (observed as an empty 431 reply). Tune with `REST_HOST_HEADER`/`--rest-host-header` (`auto`/`none`/verbatim value).
|
||||
- PostgreSQL tests are skipped unless `--pg-host` and `--pg-user` are provided. With the compose stack PG is always present (published on `5434`).
|
||||
- `test_08` and `test_12`'s execution tests run `docker compose exec` against the running `prosody` container; they skip when the compose stack is not up.
|
||||
- The healthcheck test now **executes** `healthcheck.sh` inside the container with a tampered cert and asserts exit 2 (replacing the earlier source-grep check), plus a no-residual-`${...}` config-rendering assertion.
|
||||
|
||||
+40
-4
@@ -258,9 +258,8 @@ class RESTInjector:
|
||||
self.url = url
|
||||
self.auth_user = auth_user
|
||||
self.auth_password = auth_password
|
||||
# Prosody routes HTTP by the Host header to a virtual host; mod_http_rest
|
||||
# lives on the XMPP domain's vhost, so requests to a generic host/port
|
||||
# (e.g. http://localhost:5280/rest) must carry Host: <xmpp domain>.
|
||||
# See _resolve_rest_host_header: only direct (IP/localhost) prosody
|
||||
# access needs a Host: <xmpp domain> override for vhost routing.
|
||||
self.host_header = host_header
|
||||
|
||||
def _headers(self):
|
||||
@@ -344,6 +343,16 @@ def pytest_addoption(parser):
|
||||
"--rest-password", action="store", default=os.getenv("REST_PASSWORD", ""),
|
||||
help="HTTP Basic Auth password for /rest endpoint (reverse proxy credential)"
|
||||
)
|
||||
parser.addoption(
|
||||
"--rest-host-header", action="store", default=os.getenv("REST_HOST_HEADER", "auto"),
|
||||
help=(
|
||||
"Host header to send with /rest requests. 'auto' (default): send the XMPP "
|
||||
"domain only when the REST URL host is an IP or localhost (direct prosody "
|
||||
"access needs it for vhost routing; ingress-fronted hostnames rewrite Host "
|
||||
"themselves and reject a mismatching override, e.g. with 431). "
|
||||
"'none': never send. Any other value: send it verbatim."
|
||||
)
|
||||
)
|
||||
parser.addoption(
|
||||
"--muc-domain", action="store", default=os.getenv("MUC_DOMAIN", ""),
|
||||
help="MUC component domain (e.g. conference.example.com)"
|
||||
@@ -405,6 +414,7 @@ def xmpp_config(request):
|
||||
"rest_url": request.config.getoption("--rest-url"),
|
||||
"rest_user": request.config.getoption("--rest-user"),
|
||||
"rest_password": request.config.getoption("--rest-password"),
|
||||
"rest_host_header": request.config.getoption("--rest-host-header"),
|
||||
"muc_domain": request.config.getoption("--muc-domain"),
|
||||
"admin_telnet_host": request.config.getoption("--admin-telnet-host"),
|
||||
"admin_telnet_port": request.config.getoption("--admin-telnet-port"),
|
||||
@@ -468,6 +478,30 @@ async def second_client(xmpp_config):
|
||||
await client.async_disconnect()
|
||||
|
||||
|
||||
def _resolve_rest_host_header(mode, url, domain):
|
||||
"""Decide which Host header (if any) to send with /rest requests.
|
||||
|
||||
Direct prosody access (IP/localhost URL) needs Host: <xmpp domain> because
|
||||
prosody routes HTTP to a vhost by Host and has no default_host. An
|
||||
ingress-fronted DNS name rewrites Host for the upstream itself, and
|
||||
rejects a request whose Host doesn't match the TLS name (observed: 431
|
||||
with an empty body) -- so there the override must not be sent.
|
||||
"""
|
||||
if mode == "none":
|
||||
return None
|
||||
if mode and mode != "auto":
|
||||
return mode
|
||||
import ipaddress
|
||||
from urllib.parse import urlparse
|
||||
host = urlparse(url).hostname or ""
|
||||
try:
|
||||
ipaddress.ip_address(host)
|
||||
is_direct = True
|
||||
except ValueError:
|
||||
is_direct = host == "localhost"
|
||||
return domain if is_direct else None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rest_injector(xmpp_config):
|
||||
url = xmpp_config.get("rest_url")
|
||||
@@ -477,7 +511,9 @@ def rest_injector(xmpp_config):
|
||||
url,
|
||||
auth_user=xmpp_config.get("rest_user"),
|
||||
auth_password=xmpp_config.get("rest_password"),
|
||||
host_header=xmpp_config.get("domain"),
|
||||
host_header=_resolve_rest_host_header(
|
||||
xmpp_config.get("rest_host_header", "auto"), url, xmpp_config.get("domain")
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user