fix(bootstrap): self-heal stale /aiui/api/web-search proxy to the gated daemon

Live on archi-dev-box: the node still proxied web-search straight to
SearXNG :8888 unauthenticated — the repo conf was fixed in d0c9ea6e but
existing nodes' /etc/nginx/sites-enabled never gets rewritten by a source
edit. Added to the nginx self-heal battery: stale 8888/search proxy_pass →
session-gated 5678 with the Cookie forwarded (heal_stale_web_search_block,
pure + idempotent + tested). Fresh ISOs already ship the gated block.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-07 16:54:38 -04:00
co-authored by Claude
parent 4361a5cba7
commit 482c4e300e
+36
View File
@@ -1055,6 +1055,22 @@ async fn run_nginx() -> Result<bool> {
/// rejected. Stripped during nginx bootstrap so the backend solely owns CORS.
const NGINX_LND_DUP_CORS: &str = " add_header Access-Control-Allow-Origin $http_origin always;\n add_header Access-Control-Allow-Credentials \"true\" always;\n";
/// S4 follow-up (2026-08-07): pre-fix nodes proxy /aiui/api/web-search
/// STRAIGHT to SearXNG (127.0.0.1:8888) with no session check — anyone on the
/// LAN can run searches attributed to the node's IP. The canonical conf routes
/// it through the session-gated daemon (5678) and forwards the Cookie. The
/// stale target string is unique to that block, so a plain replace is safe.
/// Pure and testable; `None` when the stale shape is absent.
fn heal_stale_web_search_block(content: &str) -> Option<String> {
if !content.contains("proxy_pass http://127.0.0.1:8888/search;") {
return None;
}
Some(content.replace(
"proxy_pass http://127.0.0.1:8888/search;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;",
"proxy_pass http://127.0.0.1:5678;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header Cookie $http_cookie;",
))
}
async fn patch_nginx_conf(path: &str) -> Result<bool> {
let content = fs::read_to_string(path)
.await
@@ -1084,6 +1100,7 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
content.contains("listen 80 default_server;") && !content.contains("listen [::]:80");
let missing_v6_https =
content.contains("listen 443 ssl default_server;") && !content.contains("listen [::]:443");
let stale_web_search = heal_stale_web_search_block(&content).is_some();
if !missing_app_catalog
&& !missing_bitcoin_status
&& !missing_lnd_proxy
@@ -1093,12 +1110,18 @@ async fn patch_nginx_conf(path: &str) -> Result<bool> {
&& !needs_fedimint_css
&& !missing_v6_http
&& !missing_v6_https
&& !stale_web_search
{
return Ok(false);
}
let mut patched = content.clone();
if let Some(p) = heal_stale_web_search_block(&patched) {
patched = p;
}
if missing_v6_http {
patched = patched.replace(
"listen 80 default_server;",
@@ -1270,6 +1293,19 @@ mod tests {
let outcome = PodmanHealOutcome::Unhealthy;
assert_ne!(outcome, PodmanHealOutcome::Healthy);
}
#[test]
fn stale_web_search_block_is_gated_and_idempotent() {
let stale = " location /aiui/api/web-search {\n proxy_pass http://127.0.0.1:8888/search;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_connect_timeout 30s;\n }";
let healed = heal_stale_web_search_block(stale).expect("stale block must heal");
assert!(healed.contains("proxy_pass http://127.0.0.1:5678;"));
assert!(healed.contains("proxy_set_header Cookie $http_cookie;"));
assert!(!healed.contains("8888"));
// Second pass is a no-op (idempotent self-heal).
assert!(heal_stale_web_search_block(&healed).is_none());
// A config without the block is untouched.
assert!(heal_stale_web_search_block("location / { try_files $uri /index.html; }").is_none());
}
}
/// Repair this node's own systemd restart policy.