fix: bypass proxies in ctl startup health probe

This commit is contained in:
ai-ag2026 committed 2026-07-18 13:53:21 +02:00
1 parent ee7af85884
commit 7ab9f6ee5c
3 files changed
+100 -16

No files matched your search

+1 -2
View File
@@ -626,8 +626,7 @@ start_cmd() {
rm -f "${PID_FILE}" "${STATE_FILE}" rm -f "${PID_FILE}" "${STATE_FILE}"
return 1 return 1
fi fi
if http_proxy='' https_proxy='' HTTP_PROXY='' HTTPS_PROXY='' \ if hermes_webui_probe_health "${probe_host}" "${CTL_PORT}" "/health" 1 direct >/dev/null 2>&1; then
hermes_webui_probe_health "${probe_host}" "${CTL_PORT}" "/health" 1 >/dev/null 2>&1; then
healthy=1 healthy=1
break break
fi fi
+32 -14
View File
@@ -58,31 +58,49 @@ _hermes_webui_warn_self_signed() {
"$1" >&2 "$1" >&2
} }
# _hermes_webui_http_get <url> <max_time> <mode> # _hermes_webui_http_get <url> <max_time> <mode> [direct]
# mode: "insecure" disables certificate verification, anything else verifies. # mode: "insecure" disables certificate verification, anything else verifies.
# direct: "direct" bypasses every configured proxy.
# Prints the response body to stdout; returns the underlying client exit code. # Prints the response body to stdout; returns the underlying client exit code.
_hermes_webui_http_get() { _hermes_webui_http_get() {
local url="$1" max_time="$2" mode="$3" local url="$1" max_time="$2" mode="$3" direct="${4:-}"
if command -v curl >/dev/null 2>&1; then if command -v curl >/dev/null 2>&1; then
if [[ "${mode}" == "insecure" ]]; then if [[ "${mode}" == "insecure" ]]; then
curl -fsS -k --max-time "${max_time}" "${url}" 2>/dev/null if [[ "${direct}" == "direct" ]]; then
curl -fsS -k --noproxy '*' --max-time "${max_time}" "${url}" 2>/dev/null
else
curl -fsS -k --max-time "${max_time}" "${url}" 2>/dev/null
fi
else else
curl -fsS --max-time "${max_time}" "${url}" 2>/dev/null if [[ "${direct}" == "direct" ]]; then
curl -fsS --noproxy '*' --max-time "${max_time}" "${url}" 2>/dev/null
else
curl -fsS --max-time "${max_time}" "${url}" 2>/dev/null
fi
fi fi
return $? return $?
elif command -v wget >/dev/null 2>&1; then elif command -v wget >/dev/null 2>&1; then
if [[ "${mode}" == "insecure" ]]; then if [[ "${mode}" == "insecure" ]]; then
wget -qO- --no-check-certificate "--timeout=${max_time}" --tries=1 "${url}" 2>/dev/null if [[ "${direct}" == "direct" ]]; then
wget -qO- --no-proxy --no-check-certificate "--timeout=${max_time}" --tries=1 "${url}" 2>/dev/null
else
wget -qO- --no-check-certificate "--timeout=${max_time}" --tries=1 "${url}" 2>/dev/null
fi
else else
wget -qO- "--timeout=${max_time}" --tries=1 "${url}" 2>/dev/null if [[ "${direct}" == "direct" ]]; then
wget -qO- --no-proxy "--timeout=${max_time}" --tries=1 "${url}" 2>/dev/null
else
wget -qO- "--timeout=${max_time}" --tries=1 "${url}" 2>/dev/null
fi
fi fi
return $? return $?
fi fi
return 127 return 127
} }
# hermes_webui_probe_health <host> <port> [path] [max_time] # hermes_webui_probe_health <host> <port> [path] [max_time] [direct]
# Prints the response body to stdout on success; warnings go to stderr. # Pass "direct" to bypass every configured proxy for all probe attempts.
# Prints the response body to stdout; warnings go to stderr.
# Returns 0 if the server answered, 1 otherwise. # Returns 0 if the server answered, 1 otherwise.
# #
# Side effect: sets the global _HERMES_WEBUI_PROBE_SCHEME to the scheme that # Side effect: sets the global _HERMES_WEBUI_PROBE_SCHEME to the scheme that
@@ -91,14 +109,14 @@ _hermes_webui_http_get() {
# back to plain HTTP when the cert/key are unloadable — so the configured # back to plain HTTP when the cert/key are unloadable — so the configured
# scheme can be https:// while the live server speaks http://. # scheme can be https:// while the live server speaks http://.
hermes_webui_probe_health() { hermes_webui_probe_health() {
local host="$1" port="$2" path="${3:-/health}" max_time="${4:-2}" local host="$1" port="$2" path="${3:-/health}" max_time="${4:-2}" direct="${5:-}"
local scheme body local scheme body
scheme="$(hermes_webui_probe_scheme)" scheme="$(hermes_webui_probe_scheme)"
local http_url="http://${host}:${port}${path}" local http_url="http://${host}:${port}${path}"
if [[ "${scheme}" == "http" ]]; then if [[ "${scheme}" == "http" ]]; then
if body="$(_hermes_webui_http_get "${http_url}" "${max_time}" "")"; then if body="$(_hermes_webui_http_get "${http_url}" "${max_time}" "" "${direct}")"; then
_HERMES_WEBUI_PROBE_SCHEME="http" _HERMES_WEBUI_PROBE_SCHEME="http"
printf '%s' "${body}" printf '%s' "${body}"
return 0 return 0
@@ -111,20 +129,20 @@ hermes_webui_probe_health() {
if _hermes_webui_truthy "${HERMES_WEBUI_TLS_INSECURE_PROBE:-}"; then if _hermes_webui_truthy "${HERMES_WEBUI_TLS_INSECURE_PROBE:-}"; then
# Explicit opt-in: skip verification, stay silent by contract. # Explicit opt-in: skip verification, stay silent by contract.
if body="$(_hermes_webui_http_get "${https_url}" "${max_time}" "insecure")"; then if body="$(_hermes_webui_http_get "${https_url}" "${max_time}" "insecure" "${direct}")"; then
_HERMES_WEBUI_PROBE_SCHEME="https" _HERMES_WEBUI_PROBE_SCHEME="https"
printf '%s' "${body}" printf '%s' "${body}"
return 0 return 0
fi fi
else else
# 1) Verified HTTPS. # 1) Verified HTTPS.
if body="$(_hermes_webui_http_get "${https_url}" "${max_time}" "")"; then if body="$(_hermes_webui_http_get "${https_url}" "${max_time}" "" "${direct}")"; then
_HERMES_WEBUI_PROBE_SCHEME="https" _HERMES_WEBUI_PROBE_SCHEME="https"
printf '%s' "${body}" printf '%s' "${body}"
return 0 return 0
fi fi
# 2) Self-signed fallback: verification failed, retry unverified + warn. # 2) Self-signed fallback: verification failed, retry unverified + warn.
if body="$(_hermes_webui_http_get "${https_url}" "${max_time}" "insecure")"; then if body="$(_hermes_webui_http_get "${https_url}" "${max_time}" "insecure" "${direct}")"; then
_hermes_webui_warn_self_signed "${https_url}" _hermes_webui_warn_self_signed "${https_url}"
_HERMES_WEBUI_PROBE_SCHEME="https" _HERMES_WEBUI_PROBE_SCHEME="https"
printf '%s' "${body}" printf '%s' "${body}"
@@ -133,7 +151,7 @@ hermes_webui_probe_health() {
fi fi
# 3) server.py may have fallen back to plain HTTP (cert/key unloadable). # 3) server.py may have fallen back to plain HTTP (cert/key unloadable).
if body="$(_hermes_webui_http_get "${http_url}" "${max_time}" "")"; then if body="$(_hermes_webui_http_get "${http_url}" "${max_time}" "" "${direct}")"; then
_HERMES_WEBUI_PROBE_SCHEME="http" _HERMES_WEBUI_PROBE_SCHEME="http"
printf '%s' "${body}" printf '%s' "${body}"
return 0 return 0
+67
View File
@@ -430,6 +430,73 @@ def test_port_guard_ignores_http_proxy_env(tmp_path):
_stop_proc(server) _stop_proc(server)
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX daemon guards")
def test_start_health_probe_ignores_all_proxy_env(tmp_path):
"""The startup watch must reach its own local health server directly.
Unlike the preflight ownership guard above, this drives the post-launch
health loop: a dead ALL_PROXY must not make ctl wait out its grace period
and print the misleading health-timeout note.
"""
port = _free_port()
health_server = tmp_path / "health-server"
health_server.write_text(
textwrap.dedent(
"""
#!/usr/bin/env python3
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
class HealthHandler(BaseHTTPRequestHandler):
def do_GET(self):
body = b'{"status": "ok"}'
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
HTTPServer(("127.0.0.1", int(sys.argv[-1])), HealthHandler).serve_forever()
"""
).lstrip(),
encoding="utf-8",
)
health_server.chmod(0o755)
started_pid = None
try:
result = run_ctl(
tmp_path,
"start",
env=_guard_env(
HERMES_WEBUI_PORT=str(port),
HERMES_WEBUI_PYTHON=str(health_server),
HERMES_WEBUI_CTL_ALLOW_SYSTEMD_CONFLICT="1",
# A live local server must still win when both proxy spellings
# route clients to a dead endpoint.
ALL_PROXY="http://127.0.0.1:1",
all_proxy="http://127.0.0.1:1",
NO_PROXY="",
no_proxy="",
),
timeout=15,
)
combined = result.stdout + result.stderr
pid_file = tmp_path / ".hermes" / "webui.pid"
if pid_file.exists():
started_pid = int(pid_file.read_text().strip())
assert result.returncode == 0, combined
assert "/health did not respond" not in combined
finally:
if started_pid:
try:
os.kill(started_pid, 9)
except ProcessLookupError:
pass
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX daemon guards") @pytest.mark.skipif(sys.platform == "win32", reason="POSIX daemon guards")
def test_ipv6_host_probe_builds_bracketed_url(tmp_path): def test_ipv6_host_probe_builds_bracketed_url(tmp_path):
"""HERMES_WEBUI_HOST='::1' must probe http://[::1]:port — the unbracketed """HERMES_WEBUI_HOST='::1' must probe http://[::1]:port — the unbracketed