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", // Onboarding flow (before user has a session — DID creation, signing, backup) "node.did", "node.signChallenge", "node.nostr-pubkey", "node.createBackup", "identity.verify", "identity.resolve-did", // 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", "must be", "cannot", "Password", "Session", ]; 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)) }