testsAndMisc/linux_configuration/scripts/single_use/features/lib/searxng_start.sh
Krzysztof kuhy Rudnicki d9c315e3ef Split setup_searxng.sh 912 -> 184
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
2026-08-16 19:11:40 +02:00

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"
}