//! Federation state sync and remote deployment. use anyhow::{Context, Result}; use std::path::Path; use super::storage::update_node_state; use super::types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel}; /// Sync state with a single federated peer over Tor. pub async fn sync_with_peer( data_dir: &Path, peer: &FederatedNode, local_did: &str, sign_fn: impl FnOnce(&[u8]) -> String, ) -> Result { let host = if peer.onion.ends_with(".onion") { peer.onion.clone() } else { format!("{}.onion", peer.onion) }; let url = format!("http://{}/rpc/v1", host); // Sign current timestamp for authentication let timestamp = chrono::Utc::now().to_rfc3339(); let signature = sign_fn(timestamp.as_bytes()); let body = serde_json::json!({ "method": "federation.get-state", "params": {} }); let proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY).context("Invalid Tor proxy")?; let client = reqwest::Client::builder() .proxy(proxy) .timeout(std::time::Duration::from_secs(30)) .build() .context("Failed to build HTTP client")?; let resp = client .post(&url) .header("X-Federation-DID", local_did) .header("X-Federation-Sig", &signature) .header("X-Federation-Timestamp", ×tamp) .json(&body) .send() .await .context("Failed to reach federated peer")?; if !resp.status().is_success() { anyhow::bail!("Peer returned {}", resp.status()); } let result: serde_json::Value = resp.json().await.context("Invalid response from peer")?; let state_val = result .get("result") .ok_or_else(|| anyhow::anyhow!("No result in peer response"))?; let state: NodeStateSnapshot = serde_json::from_value(state_val.clone()).context("Failed to parse peer state")?; update_node_state(data_dir, &peer.did, state.clone()).await?; Ok(state) } /// Build the local node's state snapshot for sharing with peers. pub fn build_local_state( apps: Vec, cpu: f64, mem_used: u64, mem_total: u64, disk_used: u64, disk_total: u64, uptime: u64, tor_active: bool, server_name: Option, ) -> NodeStateSnapshot { NodeStateSnapshot { timestamp: chrono::Utc::now().to_rfc3339(), node_name: server_name, apps, cpu_usage_percent: Some(cpu), mem_used_bytes: Some(mem_used), mem_total_bytes: Some(mem_total), disk_used_bytes: Some(disk_used), disk_total_bytes: Some(disk_total), uptime_secs: Some(uptime), tor_active: Some(tor_active), } } /// Deploy an app to a remote federated peer over Tor. /// Only works if the peer is trusted and the app exists in our marketplace. pub async fn deploy_to_peer( peer: &FederatedNode, app_id: &str, version: &str, marketplace_url: &str, local_did: &str, sign_fn: impl FnOnce(&[u8]) -> String, ) -> Result { if peer.trust_level != TrustLevel::Trusted { anyhow::bail!("Can only deploy to trusted peers (current: {})", peer.trust_level); } let host = if peer.onion.ends_with(".onion") { peer.onion.clone() } else { format!("{}.onion", peer.onion) }; let url = format!("http://{}/rpc/v1", host); let timestamp = chrono::Utc::now().to_rfc3339(); let signature = sign_fn(timestamp.as_bytes()); let body = serde_json::json!({ "method": "package.install", "params": { "id": app_id, "version": version, "marketplace-url": marketplace_url, } }); let proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY).context("Invalid Tor proxy")?; let client = reqwest::Client::builder() .proxy(proxy) .timeout(std::time::Duration::from_secs(120)) .build() .context("Failed to build HTTP client")?; let resp = client .post(&url) .header("X-Federation-DID", local_did) .header("X-Federation-Sig", &signature) .header("X-Federation-Timestamp", ×tamp) .json(&body) .send() .await .context("Failed to reach federated peer for deploy")?; if !resp.status().is_success() { anyhow::bail!("Remote node returned HTTP {}", resp.status()); } let result: serde_json::Value = resp.json().await.context("Invalid response from peer")?; if let Some(err) = result.get("error") { if !err.is_null() { let msg = err.get("message").and_then(|m| m.as_str()).unwrap_or("Unknown remote error"); anyhow::bail!("Remote node refused deploy: {}", msg); } } Ok(serde_json::json!({ "deployed": true, "app_id": app_id, "peer_did": peer.did, "peer_onion": peer.onion, })) } #[cfg(test)] mod tests { use super::*; #[test] fn test_build_local_state() { let state = build_local_state( vec![AppStatus { id: "lnd".to_string(), status: "running".to_string(), version: Some("0.18".to_string()), }], 25.5, 2_000_000_000, 8_000_000_000, 100_000_000_000, 500_000_000_000, 3600, true, Some("Test Node".to_string()), ); assert_eq!(state.apps.len(), 1); assert_eq!(state.cpu_usage_percent, Some(25.5)); assert_eq!(state.tor_active, Some(true)); assert_eq!(state.node_name, Some("Test Node".to_string())); } }