use crate::session::SessionStore; use std::net::IpAddr; /// Methods that do not require a valid session cookie. pub(super) const UNAUTHENTICATED_METHODS: &[&str] = &[ "auth.login", "auth.login.totp", "auth.login.backup", "auth.isOnboardingComplete", "auth.isSetup", "auth.setup", "auth.onboardingComplete", "health", // Server readiness check (Login.vue polls this before showing form) "server.echo", // Onboarding flow (before user has a session — DID creation, signing, backup) "node.did", "node.signChallenge", "node.nostr-pubkey", "node.createBackup", "identity.create", "identity.verify", "identity.resolve-did", // Seed management (onboarding — before user has a session) "seed.generate", "seed.verify", "seed.restore", "seed.save-encrypted", // Onboarding restore (before user account exists) "backup.restore-identity", // Inter-node RPC: called by federated peers over Tor, no session cookies "federation.peer-joined", "federation.peer-address-changed", "federation.peer-did-changed", "federation.get-state", // Fleet telemetry ingest: called by remote nodes posting reports "telemetry.ingest", ]; /// Methods whose responses can be cached for a few seconds. pub(super) const CACHEABLE_METHODS: &[&str] = &["system.stats", "federation.list-nodes"]; /// Sanitize error messages before returning to clients. /// Keeps user-facing validation errors but strips internal system details. pub(super) fn sanitize_error_message(msg: &str) -> String { // Allow known validation errors through (these are user-actionable) let user_facing_prefixes = [ "Invalid", "Missing", "Not found", "Already exists", "Rate limit", "Unauthorized", "Forbidden", "Not supported", "Requires", "requires", "must be", "cannot", "Password", "Session", "Failed to pull", "Failed to start", "Container", "Image", "Bitcoin address", "No router", "No OpenWrt", "No space left", "TollGate installation failed", "No pre-built TollGate", "opkg not found", "apk update failed", ]; for prefix in &user_facing_prefixes { if msg.starts_with(prefix) { // Truncate long messages and strip file paths let sanitized = msg .replace("/var/lib/archipelago/", "[data]/") .replace("/usr/local/bin/", "[bin]/") .replace("/etc/", "[config]/"); return if sanitized.len() > 200 { format!("{}...", &sanitized[..200]) } else { sanitized }; } } // For all other errors, return a generic message "Operation failed. Check server logs for details.".to_string() } /// Derive a CSRF token from the session token via HMAC. /// Deterministic: same session token always produces the same CSRF token. /// Survives backend restarts because it depends only on the session token /// and the on-disk remember secret (not ephemeral state). pub(super) async fn derive_csrf_token(session_token: &str) -> String { use hmac::{Hmac, Mac}; use sha2::Sha256; type HmacSha256 = Hmac; let secret = SessionStore::load_or_create_remember_secret().await; let mut mac = HmacSha256::new_from_slice(&secret).expect("HMAC key"); mac.update(format!("csrf:{}", session_token).as_bytes()); hex::encode(mac.finalize().into_bytes()) } /// Extract a named cookie value from headers. pub(super) fn extract_cookie(headers: &hyper::HeaderMap, name: &str) -> Option { let prefix = format!("{}=", name); for value in headers.get_all("cookie") { if let Ok(s) = value.to_str() { for part in s.split(';') { let part = part.trim(); if let Some(val) = part.strip_prefix(&prefix) { let val = val.trim(); if !val.is_empty() { return Some(val.to_string()); } } } } } None } /// Extract the client IP from request headers (X-Real-IP or X-Forwarded-For). pub(super) fn extract_client_ip(headers: &hyper::HeaderMap) -> IpAddr { headers .get("x-real-ip") .or_else(|| headers.get("x-forwarded-for")) .and_then(|v| v.to_str().ok()) .and_then(|s| s.split(',').next()) .and_then(|s| s.trim().parse::().ok()) .unwrap_or(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)) }