mod analytics; mod auth; mod backup_rpc; mod bitcoin; mod container; mod content; mod credentials; mod dwn; mod federation; mod handshake; mod identity; mod interfaces; mod marketplace; mod monitoring; mod names; mod lnd; mod mesh; mod network; mod node; mod nostr; mod package; mod peers; mod router; mod security; mod tor; mod transport; mod totp; mod system; mod update; mod vpn; mod wallet; mod webhooks; use crate::auth::AuthManager; use crate::config::Config; use crate::container::DevContainerOrchestrator; use crate::monitoring::MetricsStore; use crate::port_allocator::PortAllocator; use crate::session::{self, EndpointRateLimiter, LoginRateLimiter, SessionStore, REMEMBER_TTL}; use crate::state::StateManager; use anyhow::{Context, Result}; use hyper::{Request, Response, StatusCode}; use serde::{Deserialize, Serialize}; use std::net::IpAddr; use std::sync::{Arc, Mutex}; use tracing::{debug, error}; #[derive(Debug, Deserialize)] struct RpcRequest { method: String, params: Option, } #[derive(Debug, Serialize)] struct RpcResponse { result: Option, error: Option, } #[derive(Debug, Serialize)] struct RpcError { code: i32, message: String, data: Option, } /// Default dev password when no user is set up (matches mock-backend). pub(crate) const DEV_DEFAULT_PASSWORD: &str = "password123"; /// Sanitize error messages before returning to clients. /// Keeps user-facing validation errors but strips internal system details. 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() } /// Methods that do not require a valid session cookie. const UNAUTHENTICATED_METHODS: &[&str] = &[ "auth.login", "auth.login.totp", "auth.login.backup", "auth.isOnboardingComplete", "auth.isSetup", "health", // 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", ]; /// Simple TTL cache for read-only RPC responses. struct ResponseCache { entries: tokio::sync::RwLock>, ttl: std::time::Duration, } impl ResponseCache { fn new(ttl_secs: u64) -> Self { Self { entries: tokio::sync::RwLock::new(std::collections::HashMap::new()), ttl: std::time::Duration::from_secs(ttl_secs), } } async fn get(&self, key: &str) -> Option { let entries = self.entries.read().await; if let Some((ts, value)) = entries.get(key) { if ts.elapsed() < self.ttl { return Some(value.clone()); } } None } async fn set(&self, key: String, value: serde_json::Value) { let mut entries = self.entries.write().await; entries.insert(key, (std::time::Instant::now(), value)); } } /// Methods whose responses can be cached for a few seconds. const CACHEABLE_METHODS: &[&str] = &[ "system.stats", "federation.list-nodes", ]; pub struct RpcHandler { config: Config, auth_manager: AuthManager, orchestrator: Option>, state_manager: Arc, pub(crate) metrics_store: Arc, port_allocator: Arc>, pub session_store: SessionStore, login_rate_limiter: LoginRateLimiter, endpoint_rate_limiter: EndpointRateLimiter, response_cache: ResponseCache, mesh_service: Arc>>, transport_router: Arc>>>, } impl RpcHandler { pub async fn new( config: Config, state_manager: Arc, metrics_store: Arc, session_store: SessionStore, ) -> Result { let auth_manager = AuthManager::new(config.data_dir.clone()); let orchestrator = if config.dev_mode { Some(Arc::new( DevContainerOrchestrator::new(config.clone()).await?, )) } else { None }; let port_allocator = Arc::new(Mutex::new(PortAllocator::new(&config.data_dir)?)); Ok(Self { config, auth_manager, orchestrator, state_manager, metrics_store, port_allocator, session_store, login_rate_limiter: LoginRateLimiter::new(), endpoint_rate_limiter: EndpointRateLimiter::new(), response_cache: ResponseCache::new(5), mesh_service: Arc::new(tokio::sync::RwLock::new(None)), transport_router: Arc::new(tokio::sync::RwLock::new(None)), }) } /// Set the mesh service (called after identity is loaded). pub async fn set_mesh_service(&self, service: crate::mesh::MeshService) { *self.mesh_service.write().await = Some(service); } /// Set the transport router (called after all transports are initialized). pub async fn set_transport_router(&self, router: Arc) { *self.transport_router.write().await = Some(router); } /// Get reference to the mesh service Arc (for MeshTransport wrapper). pub fn mesh_service_arc(&self) -> Arc>> { Arc::clone(&self.mesh_service) } fn cookie_suffix(&self) -> &'static str { if self.config.dev_mode { "" } else { "; Secure" } } pub async fn handle( &self, req: Request, ) -> Result> { // Extract session cookie before consuming the request let (parts, body) = req.into_parts(); let session_token = session::extract_session_cookie(&parts.headers); let body_bytes = hyper::body::to_bytes(body).await .context("Failed to read body")?; let rpc_req: RpcRequest = serde_json::from_slice(&body_bytes) .context("Invalid RPC request")?; debug!("RPC method: {}", rpc_req.method); // Enforce authentication for non-allowlisted methods let is_unauthenticated = UNAUTHENTICATED_METHODS.contains(&rpc_req.method.as_str()); let mut new_session_cookies: Option<(String, String)> = None; // (session, csrf) if auto-restored if !is_unauthenticated { let mut authenticated = match &session_token { Some(token) => self.session_store.validate(token).await, None => false, }; // If session invalid, try remember-me token to auto-restore session if !authenticated { if let Some(remember) = extract_cookie(&parts.headers, "remember") { if crate::session::SessionStore::validate_remember_token(&remember) { // Auto-create a new session from the remember-me token let new_token = self.session_store.create().await; let new_csrf = derive_csrf_token(&new_token); tracing::info!("Auto-restored session from remember-me token"); new_session_cookies = Some((new_token, new_csrf)); authenticated = true; } } } if !authenticated { let reason = if session_token.is_none() { "no session cookie" } else { "invalid/expired token" }; tracing::warn!(method = %rpc_req.method, reason, "401 Unauthorized — rejecting RPC call"); let rpc_resp = RpcResponse { result: None, error: Some(RpcError { code: 401, message: "Unauthorized".to_string(), data: None, }), }; let resp_body = serde_json::to_vec(&rpc_resp) .context("Failed to serialize response")?; return Ok(Response::builder() .status(StatusCode::UNAUTHORIZED) .header("Content-Type", "application/json") .body(hyper::Body::from(resp_body)) .unwrap()); } } // RBAC: check if the user's role allows this method if !is_unauthenticated { if let Ok(Some(user)) = self.auth_manager.get_user().await { if !user.role.can_access(&rpc_req.method) { let rpc_resp = RpcResponse { result: None, error: Some(RpcError { code: 403, message: "Forbidden: insufficient permissions".to_string(), data: None, }), }; let resp_body = serde_json::to_vec(&rpc_resp) .context("Failed to serialize response")?; return Ok(Response::builder() .status(StatusCode::FORBIDDEN) .header("Content-Type", "application/json") .body(hyper::Body::from(resp_body)) .unwrap()); } } } // CSRF protection: validate X-CSRF-Token header via HMAC derivation from session token. // The expected CSRF value is derived deterministically from the session token, so it // survives backend restarts and eliminates cookie/header race conditions. // Skip CSRF check if session was just auto-restored from remember-me (new CSRF will be set in response). if !is_unauthenticated && new_session_cookies.is_none() { let csrf_header = parts .headers .get("x-csrf-token") .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); let csrf_valid = match (&session_token, &csrf_header) { (Some(token), Some(header)) => { // Verify using HMAC — constant-time comparison built-in use hmac::{Hmac, Mac}; use sha2::Sha256; type HmacSha256 = Hmac; let secret = SessionStore::load_or_create_remember_secret(); let mut mac = match HmacSha256::new_from_slice(&secret) { Ok(m) => m, Err(_) => { return Ok(Response::builder().status(500).body(hyper::Body::empty()).unwrap()); } }; mac.update(format!("csrf:{}", token).as_bytes()); match hex::decode(header) { Ok(header_bytes) => mac.verify_slice(&header_bytes).is_ok(), Err(_) => false, } } _ => false, }; if !csrf_valid { tracing::warn!( method = %rpc_req.method, has_session = session_token.is_some(), has_header = csrf_header.is_some(), "403 CSRF validation failed — rejecting RPC call" ); let rpc_resp = RpcResponse { result: None, error: Some(RpcError { code: 403, message: "CSRF token missing or invalid".to_string(), data: None, }), }; let resp_body = serde_json::to_vec(&rpc_resp) .context("Failed to serialize response")?; return Ok(Response::builder() .status(StatusCode::FORBIDDEN) .header("Content-Type", "application/json") .body(hyper::Body::from(resp_body)) .unwrap()); } } // Rate limit login attempts if rpc_req.method == "auth.login" { let client_ip = extract_client_ip(&parts.headers); if !self.login_rate_limiter.check(client_ip).await { let rpc_resp = RpcResponse { result: None, error: Some(RpcError { code: 429, message: "Too many login attempts. Try again later.".to_string(), data: None, }), }; let resp_body = serde_json::to_vec(&rpc_resp) .context("Failed to serialize response")?; return Ok(Response::builder() .status(StatusCode::TOO_MANY_REQUESTS) .header("Content-Type", "application/json") .header("Retry-After", "60") .body(hyper::Body::from(resp_body)) .unwrap()); } } // Rate limit sensitive endpoints (wallet, identity, backup, container, etc.) { let client_ip = extract_client_ip(&parts.headers); if !self.endpoint_rate_limiter.check(&rpc_req.method, client_ip).await { let rpc_resp = RpcResponse { result: None, error: Some(RpcError { code: 429, message: "Rate limit exceeded for this operation. Try again later.".to_string(), data: None, }), }; let resp_body = serde_json::to_vec(&rpc_resp) .context("Failed to serialize response")?; return Ok(Response::builder() .status(StatusCode::TOO_MANY_REQUESTS) .header("Content-Type", "application/json") .header("Retry-After", "60") .body(hyper::Body::from(resp_body)) .unwrap()); } self.endpoint_rate_limiter.record(&rpc_req.method, client_ip).await; } // Extract params; clone for post-routing use (login 2FA check needs password) let params = rpc_req.params; let login_params: Option = if rpc_req.method == "auth.login" { params.clone() } else { None }; // Check cache for cacheable methods let is_cacheable = CACHEABLE_METHODS.contains(&rpc_req.method.as_str()); if is_cacheable { if let Some(cached) = self.response_cache.get(&rpc_req.method).await { let rpc_resp = RpcResponse { result: Some(cached), error: None, }; return Ok(Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(hyper::Body::from(serde_json::to_string(&rpc_resp)?)) .unwrap()); } } // Route to handler (track latency for metrics) let rpc_start = std::time::Instant::now(); let result = match rpc_req.method.as_str() { "echo" => self.handle_echo(params).await, "server.echo" => self.handle_echo(params).await, "health" => self.handle_health().await, "auth.login" => self.handle_auth_login(params).await, "auth.logout" => self.handle_auth_logout().await, "auth.changePassword" => self.handle_auth_change_password(params, &session_token).await, "auth.onboardingComplete" => self.handle_auth_onboarding_complete().await, "auth.isOnboardingComplete" => self.handle_auth_is_onboarding_complete().await, "auth.resetOnboarding" => self.handle_auth_reset_onboarding(params).await, // Container orchestration (for Archipelago-managed containers) "container-install" => self.handle_container_install(params).await, "container-start" => self.handle_container_start(params).await, "container-stop" => self.handle_container_stop(params).await, "container-remove" => self.handle_container_remove(params).await, "container-list" => self.handle_container_list().await, "container-status" => self.handle_container_status(params).await, "container-logs" => self.handle_container_logs(params).await, "container-health" => self.handle_container_health(params).await, // Package management (for docker-compose apps) "package.install" => self.handle_package_install(params).await, "package.start" => self.handle_package_start(params).await, "package.stop" => self.handle_package_stop(params).await, "package.restart" => self.handle_package_restart(params).await, "package.uninstall" => self.handle_package_uninstall(params).await, // Bundled app management (for pre-loaded container images) "bundled-app-start" => self.handle_bundled_app_start(params).await, "bundled-app-stop" => self.handle_bundled_app_stop(params).await, // Node identity and P2P peers "node-add-peer" => self.handle_node_add_peer(params).await, "node-list-peers" => self.handle_node_list_peers().await, "node-remove-peer" => self.handle_node_remove_peer(params).await, "node-send-message" => self.handle_node_send_message(params).await, "node-check-peer" => self.handle_node_check_peer(params).await, "node-messages-received" => self.handle_node_messages_received().await, "node-store-sent" => self.handle_node_store_sent(params).await, "node-nostr-discover" => self.handle_node_nostr_discover().await, "node.did" => self.handle_node_did().await, "node.signChallenge" => self.handle_node_sign_challenge(params).await, "node.createBackup" => self.handle_node_create_backup(params).await, "node.tor-address" => self.handle_node_tor_address().await, "node.nostr-publish" => self.handle_node_nostr_publish().await, "node.nostr-pubkey" => self.handle_node_nostr_pubkey().await, "node.nostr-sign" => self.handle_node_nostr_sign(params).await, "node-nostr-verify-revoked" => self.handle_node_nostr_verify_revoked().await, "node.rotate-did" => self.handle_node_rotate_did(params).await, // Encrypted peer handshake (NIP-44) "handshake.discover" => self.handle_handshake_discover().await, "handshake.connect" => self.handle_handshake_connect(params).await, "handshake.poll" => self.handle_handshake_poll().await, // TOTP 2FA "auth.totp.setup.begin" => self.handle_totp_setup_begin(params).await, "auth.totp.setup.confirm" => self.handle_totp_setup_confirm(params).await, "auth.totp.disable" => self.handle_totp_disable(params).await, "auth.totp.status" => self.handle_totp_status().await, "auth.login.totp" => self.handle_login_totp(params, &session_token).await, "auth.login.backup" => self.handle_login_backup(params, &session_token).await, // Bitcoin & Lightning deep data "bitcoin.getinfo" => self.handle_bitcoin_getinfo().await, "lnd.getinfo" => self.handle_lnd_getinfo().await, "lnd.listchannels" => self.handle_lnd_listchannels().await, "lnd.openchannel" => self.handle_lnd_openchannel(params).await, "lnd.closechannel" => self.handle_lnd_closechannel(params).await, "lnd.newaddress" => self.handle_lnd_newaddress().await, "lnd.sendcoins" => self.handle_lnd_sendcoins(params).await, "lnd.createinvoice" => self.handle_lnd_createinvoice(params).await, "lnd.payinvoice" => self.handle_lnd_payinvoice(params).await, "lnd.create-psbt" => self.handle_lnd_create_psbt(params).await, "lnd.finalize-psbt" => self.handle_lnd_finalize_psbt(params).await, "lnd.create-raw-tx" => self.handle_lnd_create_raw_tx(params).await, "lnd.gettransactions" => self.handle_lnd_gettransactions().await, "lnd.connect-info" => self.handle_lnd_connect_info().await, "lnd.export-channel-backup" => self.handle_lnd_export_channel_backup().await, // Multi-identity management "identity.list" => self.handle_identity_list(params).await, "identity.create" => self.handle_identity_create(params).await, "identity.get" => self.handle_identity_get(params).await, "identity.delete" => self.handle_identity_delete(params).await, "identity.set-default" => self.handle_identity_set_default(params).await, "identity.sign" => self.handle_identity_sign(params).await, "identity.verify" => self.handle_identity_verify(params).await, "identity.resolve-did" => self.handle_identity_resolve_did(params).await, "identity.resolve-remote-did" => self.handle_identity_resolve_remote_did(params).await, "identity.verify-did-document" => self.handle_identity_verify_did_document(params).await, "identity.create-dht-did" => self.handle_identity_create_dht_did(params).await, "identity.resolve-dht-did" => self.handle_identity_resolve_dht_did(params).await, "identity.refresh-dht-did" => self.handle_identity_refresh_dht_did(params).await, "identity.dht-status" => self.handle_identity_dht_status(params).await, "identity.update-profile" => self.handle_identity_update_profile(params).await, "identity.publish-profile" => self.handle_identity_publish_profile(params).await, "identity.export-keys" => self.handle_identity_export_keys(params).await, "identity.create-nostr-key" => self.handle_identity_create_nostr_key(params).await, "identity.nostr-sign" => self.handle_identity_nostr_sign(params).await, "identity.nostr-encrypt-nip04" => self.handle_identity_nostr_encrypt_nip04(params).await, "identity.nostr-decrypt-nip04" => self.handle_identity_nostr_decrypt_nip04(params).await, "identity.nostr-encrypt-nip44" => self.handle_identity_nostr_encrypt_nip44(params).await, "identity.nostr-decrypt-nip44" => self.handle_identity_nostr_decrypt_nip44(params).await, // Bitcoin domain names (NIP-05) "identity.register-name" => self.handle_identity_register_name(params).await, "identity.remove-name" => self.handle_identity_remove_name(params).await, "identity.resolve-name" => self.handle_identity_resolve_name(params).await, "identity.list-names" => self.handle_identity_list_names(params).await, "identity.link-name" => self.handle_identity_link_name(params).await, // Verifiable Credentials "identity.issue-credential" => self.handle_identity_issue_credential(params).await, "identity.verify-credential" => self.handle_identity_verify_credential(params).await, "identity.list-credentials" => self.handle_identity_list_credentials(params).await, "identity.revoke-credential" => self.handle_identity_revoke_credential(params).await, "identity.create-presentation" => self.handle_identity_create_presentation(params).await, "identity.verify-presentation" => self.handle_identity_verify_presentation(params).await, // Network overlay "network.get-visibility" => self.handle_network_get_visibility().await, "network.set-visibility" => self.handle_network_set_visibility(params).await, "network.request-connection" => self.handle_network_request_connection(params).await, "network.list-requests" => self.handle_network_list_requests().await, "network.accept-request" => self.handle_network_accept_request(params).await, "network.reject-request" => self.handle_network_reject_request(params).await, // Tor hidden services "tor.list-services" => self.handle_tor_list_services().await, "tor.create-service" => self.handle_tor_create_service(params).await, "tor.delete-service" => self.handle_tor_delete_service(params).await, "tor.get-onion-address" => self.handle_tor_get_onion_address(params).await, "tor.rotate-service" => self.handle_tor_rotate_service(params).await, "tor.cleanup-rotated" => self.handle_tor_cleanup_rotated().await, "tor.toggle-app" => self.handle_tor_toggle_app(params).await, "tor.restart" => self.handle_tor_restart().await, // Nostr relay management "nostr.list-relays" => self.handle_nostr_list_relays().await, "nostr.add-relay" => self.handle_nostr_add_relay(params).await, "nostr.remove-relay" => self.handle_nostr_remove_relay(params).await, "nostr.toggle-relay" => self.handle_nostr_toggle_relay(params).await, "nostr.get-stats" => self.handle_nostr_get_stats().await, // Router / UPnP "router.discover" => self.handle_router_discover().await, "router.list-forwards" => self.handle_router_list_forwards().await, "router.add-forward" => self.handle_router_add_forward(params).await, "router.remove-forward" => self.handle_router_remove_forward(params).await, "network.diagnostics" => self.handle_network_diagnostics().await, "network.list-interfaces" => self.handle_network_list_interfaces().await, "network.scan-wifi" => self.handle_network_scan_wifi().await, "network.configure-wifi" => self.handle_network_configure_wifi(params).await, "network.configure-ethernet" => self.handle_network_configure_ethernet(params).await, "network.dns-status" => self.handle_network_dns_status().await, "network.configure-dns" => self.handle_network_configure_dns(params).await, "router.detect" => self.handle_router_detect(params).await, "router.info" => self.handle_router_info().await, "router.configure" => self.handle_router_configure(params).await, // Ecash wallet "wallet.ecash-balance" => self.handle_wallet_ecash_balance().await, "wallet.ecash-mint" => self.handle_wallet_ecash_mint(params).await, "wallet.ecash-melt" => self.handle_wallet_ecash_melt(params).await, "wallet.ecash-send" => self.handle_wallet_ecash_send(params).await, "wallet.ecash-receive" => self.handle_wallet_ecash_receive(params).await, "wallet.ecash-history" => self.handle_wallet_ecash_history().await, "wallet.networking-profits" => self.handle_wallet_networking_profits().await, // Content catalog management "content.list-mine" => self.handle_content_list_mine().await, "content.add" => self.handle_content_add(params).await, "content.remove" => self.handle_content_remove(params).await, "content.set-pricing" => self.handle_content_set_pricing(params).await, "content.set-availability" => self.handle_content_set_availability(params).await, "content.browse-peer" => self.handle_content_browse_peer(params).await, "content.download-peer" => self.handle_content_download_peer(params).await, // DWN (Decentralized Web Node) "dwn.status" => self.handle_dwn_status().await, "dwn.sync" => self.handle_dwn_sync().await, "dwn.register-protocol" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_dwn_register_protocol(&p).await } "dwn.list-protocols" => self.handle_dwn_list_protocols().await, "dwn.remove-protocol" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_dwn_remove_protocol(&p).await } "dwn.query-messages" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_dwn_query_messages(&p).await } "dwn.write-message" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_dwn_write_message(&p).await } // Federation "federation.invite" => self.handle_federation_invite().await, "federation.join" => self.handle_federation_join(params).await, "federation.list-nodes" => self.handle_federation_list_nodes().await, "federation.remove-node" => self.handle_federation_remove_node(params).await, "federation.set-trust" => self.handle_federation_set_trust(params).await, "federation.sync-state" => self.handle_federation_sync_state().await, "federation.get-state" => self.handle_federation_get_state().await, "federation.peer-joined" => self.handle_federation_peer_joined(params).await, "federation.deploy-app" => self.handle_federation_deploy_app(params).await, "federation.peer-address-changed" => self.handle_federation_peer_address_changed(params).await, "federation.notify-did-change" => self.handle_federation_notify_did_change(params).await, "federation.peer-did-changed" => self.handle_federation_peer_did_changed(params).await, // VPN & Remote Access "vpn.status" => self.handle_vpn_status().await, "vpn.configure" => self.handle_vpn_configure(params).await, "vpn.disconnect" => self.handle_vpn_disconnect().await, "remote.setup" => self.handle_remote_setup(params).await, // Marketplace "marketplace.discover" => self.handle_marketplace_discover().await, "marketplace.publish" => self.handle_marketplace_publish(params).await, "marketplace.get-manifest" => self.handle_marketplace_get_manifest(params).await, "marketplace.list-published" => self.handle_marketplace_list_published().await, "marketplace.verify" => self.handle_marketplace_verify(params).await, "marketplace.create-invoice" => self.handle_marketplace_create_invoice(params).await, "marketplace.check-payment" => self.handle_marketplace_check_payment(params).await, // Mesh networking (Meshcore LoRa) "mesh.status" => self.handle_mesh_status().await, "mesh.peers" => self.handle_mesh_peers().await, "mesh.messages" => self.handle_mesh_messages(params).await, "mesh.send" => self.handle_mesh_send(params).await, "mesh.broadcast" => self.handle_mesh_broadcast().await, "mesh.configure" => self.handle_mesh_configure(params).await, "mesh.send-invoice" => self.handle_mesh_send_invoice(params).await, "mesh.send-coordinate" => self.handle_mesh_send_coordinate(params).await, "mesh.send-alert" => self.handle_mesh_send_alert(params).await, "mesh.outbox" => self.handle_mesh_outbox(params).await, "mesh.session-status" => self.handle_mesh_session_status(params).await, "mesh.rotate-prekeys" => self.handle_mesh_rotate_prekeys().await, // Phase 4: Off-grid Bitcoin operations "mesh.relay-tx" => self.handle_mesh_relay_tx(params).await, "mesh.relay-status" => self.handle_mesh_relay_status(params).await, "mesh.block-headers" => self.handle_mesh_block_headers(params).await, "mesh.relay-lightning" => self.handle_mesh_relay_lightning(params).await, "mesh.deadman-status" => self.handle_mesh_deadman_status().await, "mesh.deadman-configure" => self.handle_mesh_deadman_configure(params).await, "mesh.deadman-checkin" => self.handle_mesh_deadman_checkin().await, "mesh.test-send" => self.handle_mesh_test_send(params).await, // Transport layer (unified routing) "transport.status" => self.handle_transport_status().await, "transport.peers" => self.handle_transport_peers().await, "transport.send" => self.handle_transport_send(params).await, "transport.set-mode" => self.handle_transport_set_mode(params).await, // Server settings "server.set-name" => self.handle_server_set_name(params).await, // System monitoring "system.stats" => self.handle_system_stats().await, "system.processes" => self.handle_system_processes().await, "system.temperature" => self.handle_system_temperature().await, "system.detect-usb-devices" => self.handle_system_detect_usb_devices().await, "system.disk-status" => self.handle_system_disk_status().await, "system.disk-cleanup" => self.handle_system_disk_cleanup().await, "system.reboot" => self.handle_system_reboot(params).await, "system.factory-reset" => self.handle_system_factory_reset(params).await, // Opt-in anonymous analytics "analytics.get-status" => self.handle_analytics_get_status().await, "analytics.enable" => self.handle_analytics_enable().await, "analytics.disable" => self.handle_analytics_disable().await, "analytics.get-snapshot" => self.handle_analytics_get_snapshot().await, "telemetry.report" => self.handle_telemetry_report().await, "telemetry.ingest" => self.handle_telemetry_ingest(params).await, "telemetry.fleet-status" => self.handle_telemetry_fleet_status().await, "telemetry.fleet-node-history" => self.handle_telemetry_fleet_node_history(params).await, "telemetry.fleet-alerts" => self.handle_telemetry_fleet_alerts().await, // Real-time metrics monitoring "monitoring.current" => self.handle_monitoring_current().await, "monitoring.history" => self.handle_monitoring_history(params).await, "monitoring.containers" => self.handle_monitoring_containers().await, "monitoring.alerts" => self.handle_monitoring_alerts(params).await, "monitoring.alert-rules" => self.handle_monitoring_alert_rules().await, "monitoring.configure-alert" => self.handle_monitoring_configure_alert(params).await, "monitoring.acknowledge-alert" => self.handle_monitoring_acknowledge_alert(params).await, "monitoring.export" => self.handle_monitoring_export(params).await, // System updates "update.check" => self.handle_update_check().await, "update.status" => self.handle_update_status().await, "update.dismiss" => self.handle_update_dismiss().await, "update.download" => self.handle_update_download().await, "update.apply" => self.handle_update_apply().await, "update.rollback" => self.handle_update_rollback().await, "update.get-schedule" => self.handle_update_get_schedule().await, "update.set-schedule" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_update_set_schedule(&p).await } // Backup & Restore "backup.create" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_create(&p).await } "backup.list" => self.handle_backup_list().await, "backup.verify" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_verify(&p).await } "backup.restore" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_restore(&p).await } "backup.restore-identity" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_restore_identity(&p).await } "backup.delete" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_delete(&p).await } "backup.list-drives" => self.handle_backup_list_drives().await, "backup.to-usb" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_to_usb(&p).await } "backup.upload-s3" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_upload_s3(&p).await } "backup.download-s3" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_backup_download_s3(&p).await } // Security / secrets "security.rotate-secrets" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_security_rotate_secrets(&p).await } "security.list-expiring" => { let p = params.unwrap_or(serde_json::json!({})); self.handle_security_list_expiring(&p).await } // Webhooks "webhook.get-config" => self.handle_webhook_get_config().await, "webhook.configure" => self.handle_webhook_configure(params).await, "webhook.test" => self.handle_webhook_test().await, _ => { Err(anyhow::anyhow!("Unknown method: {}", rpc_req.method)) } }; // Record RPC latency for monitoring let elapsed_ms = rpc_start.elapsed().as_secs_f64() * 1000.0; self.metrics_store.record_rpc_latency(elapsed_ms).await; // Build response (cache successful results for cacheable methods) let mut rpc_resp = match result { Ok(data) => { if is_cacheable { self.response_cache.set(rpc_req.method.clone(), data.clone()).await; } RpcResponse { result: Some(data), error: None, } } Err(e) => { error!("RPC error on {}: {}", rpc_req.method, e); // Sanitize error messages: only return user-facing text, not internal details let user_message = sanitize_error_message(&e.to_string()); RpcResponse { result: None, error: Some(RpcError { code: -1, message: user_message, data: None, }), } } }; let resp_body = serde_json::to_vec(&rpc_resp) .context("Failed to serialize response")?; let mut response = Response::builder() .status(StatusCode::OK) .header("Content-Type", "application/json") .body(hyper::Body::from(resp_body)) .unwrap(); // Track failed login attempts for rate limiting if rpc_req.method == "auth.login" && rpc_resp.error.is_some() { let client_ip = extract_client_ip(&parts.headers); self.login_rate_limiter.record_failure(client_ip).await; } // On successful login, check if 2FA is required if rpc_req.method == "auth.login" && rpc_resp.error.is_none() { let totp_enabled = self.auth_manager.is_totp_enabled().await.unwrap_or(false); if totp_enabled { // 2FA enabled: create a pending session with cached TOTP secret // We need the password to decrypt the TOTP secret for step 2 let password = login_params .as_ref() .and_then(|p| p.get("password")) .and_then(|v| v.as_str()) .unwrap_or(""); if let Ok(Some(totp_data)) = self.auth_manager.get_totp_data().await { if let Ok(secret) = crate::totp::decrypt_secret(&totp_data, password) { let token = self.session_store.create_pending(secret).await; let csrf_token = derive_csrf_token(&token); response.headers_mut().append( "Set-Cookie", format!("session={}; HttpOnly; SameSite=Lax; Path=/{}", token, self.cookie_suffix()) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!("csrf_token={}; SameSite=Lax; Path=/{}", csrf_token, self.cookie_suffix()) .parse() .unwrap(), ); // Override the response body to indicate TOTP is required let totp_body = serde_json::json!({ "result": { "requires_totp": true }, "error": null }); *response.body_mut() = hyper::Body::from( serde_json::to_vec(&totp_body).unwrap_or_default(), ); } } } else { // No 2FA: create a full session immediately let token = self.session_store.create().await; let csrf_token = derive_csrf_token(&token); let remember_token = self.session_store.create_remember_token(); response.headers_mut().append( "Set-Cookie", format!("session={}; HttpOnly; SameSite=Lax; Path=/{}", token, self.cookie_suffix()) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!("csrf_token={}; SameSite=Lax; Path=/{}", csrf_token, self.cookie_suffix()) .parse() .unwrap(), ); // Remember-me: HMAC-signed, survives backend restarts (30-day TTL) response.headers_mut().append( "Set-Cookie", format!("remember={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}", remember_token, REMEMBER_TTL, self.cookie_suffix()) .parse() .unwrap(), ); } } // On successful TOTP verification, set the rotated session cookie if (rpc_req.method == "auth.login.totp" || rpc_req.method == "auth.login.backup") && rpc_resp.error.is_none() { // Extract token (clone to release immutable borrow before mutable borrow below) let new_token_opt = rpc_resp .result .as_ref() .and_then(|r| r.get("new_session_token")) .and_then(|v| v.as_str()) .map(|s| s.to_string()); if let Some(new_token) = new_token_opt { let csrf_token = derive_csrf_token(&new_token); let remember_token = self.session_store.create_remember_token(); response.headers_mut().append( "Set-Cookie", format!( "session={}; HttpOnly; SameSite=Lax; Path=/{}", new_token, self.cookie_suffix() ) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!( "csrf_token={}; SameSite=Lax; Path=/{}", csrf_token, self.cookie_suffix() ) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!( "remember={}; HttpOnly; SameSite=Lax; Path=/; Max-Age={}{}", remember_token, REMEMBER_TTL, self.cookie_suffix() ) .parse() .unwrap(), ); // Strip the token from the response body — don't leak it to JS if let Some(result) = rpc_resp.result.as_mut() { if let Some(obj) = result.as_object_mut() { obj.remove("new_session_token"); } } let body_bytes = serde_json::to_vec(&rpc_resp).unwrap_or_default(); *response.body_mut() = hyper::Body::from(body_bytes); } } // On password change, rotate the session token for the caller if rpc_req.method == "auth.changePassword" && rpc_resp.error.is_none() { if let Some(token) = &session_token { let new_token = self.session_store.rotate(token).await; let csrf_token = derive_csrf_token(&new_token); response.headers_mut().append( "Set-Cookie", format!( "session={}; HttpOnly; SameSite=Lax; Path=/{}", new_token, self.cookie_suffix() ) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!( "csrf_token={}; SameSite=Lax; Path=/{}", csrf_token, self.cookie_suffix() ) .parse() .unwrap(), ); } } // On logout, invalidate session and expire cookies if rpc_req.method == "auth.logout" { if let Some(token) = &session_token { self.session_store.remove(token).await; } let secure_suffix = if self.config.dev_mode { "" } else { "; Secure" }; response.headers_mut().append( "Set-Cookie", format!("session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0{}", secure_suffix) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!("csrf_token=; SameSite=Lax; Path=/; Max-Age=0{}", secure_suffix) .parse() .unwrap(), ); } // If session was auto-restored from remember-me, set new cookies on the response if let Some((new_session, new_csrf)) = new_session_cookies { let suffix = self.cookie_suffix(); response.headers_mut().append( "Set-Cookie", format!("session={}; HttpOnly; SameSite=Lax; Path=/{}", new_session, suffix) .parse() .unwrap(), ); response.headers_mut().append( "Set-Cookie", format!("csrf_token={}; SameSite=Lax; Path=/{}", new_csrf, suffix) .parse() .unwrap(), ); } Ok(response) } async fn handle_echo(&self, params: Option) -> Result { if let Some(p) = params { if let Some(msg) = p.get("message").and_then(|v| v.as_str()) { return Ok(serde_json::json!({ "message": msg })); } } Ok(serde_json::json!({ "message": "Hello from Archipelago!" })) } async fn handle_health(&self) -> Result { let recovery_complete = crate::crash_recovery::is_recovery_complete(); let uptime = crate::crash_recovery::uptime_seconds(); let status = if recovery_complete { "ok" } else { "degraded" }; Ok(serde_json::json!({ "status": status, "crash_recovery_complete": recovery_complete, "uptime_seconds": uptime, "version": env!("CARGO_PKG_VERSION"), })) } } /// 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). 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(); 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. 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 csrf_token cookie value from headers. fn extract_csrf_cookie(headers: &hyper::HeaderMap) -> Option { extract_cookie(headers, "csrf_token") } /// Extract the client IP from request headers (X-Real-IP or X-Forwarded-For). 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)) }