mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 21:54:00 +01:00
Six libs: the script has four phases and write_stack alone was 342 lines because it emits three config files from heredocs. That became three functions, one per file, each keeping the comment that explains it - including why settings.yml is mode 640 and not 600. For a script whose job is writing config, "the functions still exist" is not enough. Compared the sorted non-comment lines of the original write_stack against its three successors: identical hash, so every line of emitted compose, settings.yml and limiter.toml is byte-for-byte the same and only the wrappers were added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011w2Pj4Ek3ZNMQVYcFHgXDY
106 lines
4.5 KiB
Bash
Executable File
106 lines
4.5 KiB
Bash
Executable File
#!/bin/bash
|
|
# Readiness polling, loopback assertion and stack startup.
|
|
#
|
|
# Sourced by setup_searxng.sh; split out to keep it under the 250-line
|
|
# cap. Sourced rather than run, so it inherits the caller's strict mode
|
|
# and the variables defined above the source line.
|
|
|
|
# --- Phase 3/4: start -------------------------------------------------------
|
|
# Sends the Host header the edge will actually forward, and requires a
|
|
# non-empty body. A bare "curl http://127.0.0.1:PORT/" against a host-matcher
|
|
# misconfiguration returns an empty 200 — it passes while every real request
|
|
# serves a blank page.
|
|
wait_for_site() {
|
|
local _ size
|
|
for _ in $(seq 1 60); do
|
|
# Browser-shaped headers: without them botdetection answers 429 and this
|
|
# readiness probe would spin for its full 60s against a healthy backend.
|
|
size="$(curl -s --compressed -o /dev/null -w '%{size_download}' \
|
|
-A 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
|
|
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
|
|
-H 'Accept-Language: en-US,en;q=0.9' \
|
|
-H 'Accept-Encoding: gzip, deflate, br' \
|
|
-H 'Sec-Fetch-Mode: navigate' -H 'Sec-Fetch-Site: none' -H 'Sec-Fetch-Dest: document' \
|
|
-H "Host: ${SEARX_DOMAIN}" "http://127.0.0.1:${SEARX_PORT}/" 2>/dev/null || echo 0)"
|
|
if [[ ${size:-0} -gt 0 ]]; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
wait_for_valkey() {
|
|
local _ out
|
|
for _ in $(seq 1 30); do
|
|
out="$(docker exec "$VALKEY_CONTAINER" valkey-cli -h 127.0.0.1 -p "$VALKEY_PORT" ping 2>/dev/null || true)"
|
|
if [[ $out == "PONG" ]]; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# These containers share the host network namespace with the public edge, so a
|
|
# misconfiguration does not fail locally — it quietly exposes an unfronted
|
|
# service to the whole LAN, or steals a share of :80/:443 from every other site
|
|
# on the box. Gate on it rather than trust the config to stay correct.
|
|
assert_loopback_only() {
|
|
local container="$1" port="$2" pid listeners
|
|
pid="$(docker inspect -f '{{.State.Pid}}' "$container" 2>/dev/null || echo "")"
|
|
[[ -n $pid && $pid != "0" ]] || die "Could not read the ${container} PID."
|
|
|
|
listeners="$(sudo -n ss -ltnp 2>/dev/null | grep "pid=${pid}," || true)"
|
|
|
|
# Demand positive evidence FIRST. Asserting only the absence of a bad bind
|
|
# fails open: if `ss` returns nothing — a stale PID, or sudo -n unavailable —
|
|
# every "is it bad?" test misses and the guard reports success having looked
|
|
# at nothing.
|
|
if ! grep -qE '(^|[[:space:]])127\.0\.0\.1:'"${port}"'([[:space:]]|$)' <<<"$listeners"; then
|
|
docker stop "$container" >/dev/null 2>&1 || true
|
|
die "Could not confirm ${container} listens on 127.0.0.1:${port}. Container stopped."
|
|
fi
|
|
if grep -qE ':(80|443)\b' <<<"$listeners"; then
|
|
docker stop "$container" >/dev/null 2>&1 || true
|
|
die "${container} bound :80/:443 — it would contend with ${CADDY_CONTAINER}. Container stopped."
|
|
fi
|
|
# Reject *, 0.0.0.0 AND [::] — granian's real default is ::, which a
|
|
# 0.0.0.0-only check would sail straight past.
|
|
if grep -qE '(\*|0\.0\.0\.0|\[::\]):'"${port}"'\b' <<<"$listeners"; then
|
|
docker stop "$container" >/dev/null 2>&1 || true
|
|
die "${container} bound ${port} on all interfaces, not loopback. Container stopped."
|
|
fi
|
|
log_ok "${container} listens on loopback:${port} only — no contention, not LAN-exposed."
|
|
}
|
|
|
|
start_stack() {
|
|
log_info "Starting valkey + searxng…"
|
|
docker compose -f "$SEARX_COMPOSE" up -d
|
|
# `up -d` is a no-op when only a BIND-MOUNTED config file changed — compose
|
|
# compares the compose spec, not the files it mounts. settings.yml and
|
|
# limiter.toml are read once at startup, so without an explicit restart the
|
|
# container keeps serving the previous config and every verification below
|
|
# silently tests the OLD deployment. (Hit for real: limiter.toml was written
|
|
# at 15:06 while the container had been running since 15:02, so botdetection
|
|
# kept 429ing with "missing config file".)
|
|
log_info "Restarting searxng to pick up settings.yml / limiter.toml…"
|
|
docker restart "$SEARX_CONTAINER" >/dev/null
|
|
|
|
if wait_for_valkey; then
|
|
log_ok "valkey answering PING on 127.0.0.1:${VALKEY_PORT}."
|
|
else
|
|
die "valkey did not become ready — the limiter would silently no-op."
|
|
fi
|
|
|
|
if wait_for_site; then
|
|
log_ok "SearXNG answering on http://127.0.0.1:${SEARX_PORT}/."
|
|
else
|
|
docker stop "$SEARX_CONTAINER" >/dev/null 2>&1 || true
|
|
die "SearXNG did not become reachable on 127.0.0.1:${SEARX_PORT} (container stopped)."
|
|
fi
|
|
|
|
assert_loopback_only "$VALKEY_CONTAINER" "$VALKEY_PORT"
|
|
assert_loopback_only "$SEARX_CONTAINER" "$SEARX_PORT"
|
|
}
|