fix: bypass proxies in ctl startup health probe
This commit is contained in:
1 parent
ee7af85884
commit
7ab9f6ee5c
3 files changed
+96
-12
No files matched your search
@@ -626,8 +626,7 @@ start_cmd() {
|
||||
rm -f "${PID_FILE}" "${STATE_FILE}"
|
||||
return 1
|
||||
fi
|
||||
if http_proxy='' https_proxy='' HTTP_PROXY='' HTTPS_PROXY='' \
|
||||
hermes_webui_probe_health "${probe_host}" "${CTL_PORT}" "/health" 1 >/dev/null 2>&1; then
|
||||
if hermes_webui_probe_health "${probe_host}" "${CTL_PORT}" "/health" 1 direct >/dev/null 2>&1; then
|
||||
healthy=1
|
||||
break
|
||||
fi
|
||||
|
||||
+28
-10
@@ -58,31 +58,49 @@ _hermes_webui_warn_self_signed() {
|
||||
"$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.
|
||||
# direct: "direct" bypasses every configured proxy.
|
||||
# Prints the response body to stdout; returns the underlying client exit code.
|
||||
_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 [[ "${mode}" == "insecure" ]]; then
|
||||
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
|
||||
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
|
||||
return $?
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
if [[ "${mode}" == "insecure" ]]; then
|
||||
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
|
||||
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
|
||||
return $?
|
||||
fi
|
||||
return 127
|
||||
}
|
||||
|
||||
# hermes_webui_probe_health <host> <port> [path] [max_time]
|
||||
# Prints the response body to stdout on success; warnings go to stderr.
|
||||
# hermes_webui_probe_health <host> <port> [path] [max_time] [direct]
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# scheme can be https:// while the live server speaks http://.
|
||||
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
|
||||
scheme="$(hermes_webui_probe_scheme)"
|
||||
|
||||
local http_url="http://${host}:${port}${path}"
|
||||
|
||||
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"
|
||||
printf '%s' "${body}"
|
||||
return 0
|
||||
@@ -111,20 +129,20 @@ hermes_webui_probe_health() {
|
||||
|
||||
if _hermes_webui_truthy "${HERMES_WEBUI_TLS_INSECURE_PROBE:-}"; then
|
||||
# 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"
|
||||
printf '%s' "${body}"
|
||||
return 0
|
||||
fi
|
||||
else
|
||||
# 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"
|
||||
printf '%s' "${body}"
|
||||
return 0
|
||||
fi
|
||||
# 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_PROBE_SCHEME="https"
|
||||
printf '%s' "${body}"
|
||||
@@ -133,7 +151,7 @@ hermes_webui_probe_health() {
|
||||
fi
|
||||
|
||||
# 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"
|
||||
printf '%s' "${body}"
|
||||
return 0
|
||||
|
||||
@@ -430,6 +430,73 @@ def test_port_guard_ignores_http_proxy_env(tmp_path):
|
||||
_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")
|
||||
def test_ipv6_host_probe_builds_bracketed_url(tmp_path):
|
||||
"""HERMES_WEBUI_HOST='::1' must probe http://[::1]:port — the unbracketed
|
||||
|
||||
Reference in new issue
Block a user