diff --git a/core/archipelago/src/api/rpc/content.rs b/core/archipelago/src/api/rpc/content.rs index 106d2026..07619f50 100644 --- a/core/archipelago/src/api/rpc/content.rs +++ b/core/archipelago/src/api/rpc/content.rs @@ -207,7 +207,9 @@ impl RpcHandler { Ok(serde_json::json!({ "updated": true })) } - /// Download content from a peer over Tor, returning base64-encoded data. + /// Download content from a peer. Prefers FIPS when the peer is known + /// in our federation and has advertised a FIPS npub; falls back to + /// Tor on any network failure. pub(super) async fn handle_content_download_peer( &self, params: Option, @@ -227,25 +229,19 @@ impl RpcHandler { return Err(anyhow::anyhow!("Invalid v3 onion address")); } - let socks_proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY) - .context("Failed to create SOCKS proxy")?; - - let client = reqwest::Client::builder() - .proxy(socks_proxy) - .timeout(std::time::Duration::from_secs(120)) - .build() - .context("Failed to build Tor HTTP client")?; - let (data, _) = self.state_manager.get_snapshot().await; let local_did = crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?; + let fips_npub = + crate::federation::fips_npub_for_onion(&self.config.data_dir, onion).await; - let url = format!("http://{}/content/{}", onion, content_id); - let response = client - .get(&url) - .header("X-Federation-DID", &local_did) - .send() - .await - .context("Failed to connect to peer over Tor")?; + let path = format!("/content/{}", content_id); + let (response, _transport) = + crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path) + .header("X-Federation-DID", local_did) + .timeout(std::time::Duration::from_secs(120)) + .send_get() + .await + .context("Failed to connect to peer")?; if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED { let body: serde_json::Value = response.json().await.unwrap_or_default(); @@ -273,7 +269,8 @@ impl RpcHandler { })) } - /// Browse a peer's content catalog over Tor. + /// Browse a peer's content catalog. FIPS if the peer is federated, + /// otherwise Tor. pub(super) async fn handle_content_browse_peer( &self, params: Option, @@ -289,24 +286,17 @@ impl RpcHandler { return Err(anyhow::anyhow!("Invalid v3 onion address")); } - // Connect via Tor SOCKS proxy to the peer's content catalog endpoint - let socks_proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY) - .context("Failed to create SOCKS proxy")?; + let fips_npub = + crate::federation::fips_npub_for_onion(&self.config.data_dir, onion).await; - let client = reqwest::Client::builder() - .proxy(socks_proxy) - .timeout(std::time::Duration::from_secs(30)) - .build() - .context("Failed to build Tor HTTP client")?; + debug!("Browsing peer content at {} (fips={})", onion, fips_npub.is_some()); - let url = format!("http://{}/content", onion); - debug!("Browsing peer content at {}", url); - - let response = client - .get(&url) - .send() - .await - .context("Failed to connect to peer over Tor")?; + let (response, _transport) = + crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, "/content") + .timeout(std::time::Duration::from_secs(30)) + .send_get() + .await + .context("Failed to connect to peer")?; if !response.status().is_success() { return Err(anyhow::anyhow!( @@ -354,26 +344,20 @@ impl RpcHandler { .await .context("Failed to create ecash payment token — check wallet balance")?; - let socks_proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY) - .context("Failed to create SOCKS proxy")?; - - let client = reqwest::Client::builder() - .proxy(socks_proxy) - .timeout(std::time::Duration::from_secs(120)) - .build() - .context("Failed to build Tor HTTP client")?; - let (data, _) = self.state_manager.get_snapshot().await; let local_did = crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?; + let fips_npub = + crate::federation::fips_npub_for_onion(&self.config.data_dir, onion).await; - let url = format!("http://{}/content/{}", onion, content_id); - let response = client - .get(&url) - .header("X-Federation-DID", &local_did) - .header("X-Payment-Token", &token_str) - .send() - .await - .context("Failed to connect to peer over Tor")?; + let path = format!("/content/{}", content_id); + let (response, _transport) = + crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path) + .header("X-Federation-DID", local_did) + .header("X-Payment-Token", token_str) + .timeout(std::time::Duration::from_secs(120)) + .send_get() + .await + .context("Failed to connect to peer")?; if response.status() == reqwest::StatusCode::PAYMENT_REQUIRED { // Payment was rejected — token is spent but content not received @@ -420,23 +404,18 @@ impl RpcHandler { return Err(anyhow::anyhow!("Invalid v3 onion address")); } - let socks_proxy = reqwest::Proxy::all(crate::constants::TOR_SOCKS_PROXY) - .context("Failed to create SOCKS proxy")?; + let fips_npub = + crate::federation::fips_npub_for_onion(&self.config.data_dir, onion).await; - let client = reqwest::Client::builder() - .proxy(socks_proxy) - .timeout(std::time::Duration::from_secs(30)) - .build() - .context("Failed to build Tor HTTP client")?; + let path = format!("/content/{}/preview", content_id); + debug!("Fetching content preview from {}{} (fips={})", onion, path, fips_npub.is_some()); - let url = format!("http://{}/content/{}/preview", onion, content_id); - debug!("Fetching content preview from {}", url); - - let response = client - .get(&url) - .send() - .await - .context("Failed to connect to peer for preview")?; + let (response, _transport) = + crate::fips::dial::PeerRequest::new(fips_npub.as_deref(), onion, &path) + .timeout(std::time::Duration::from_secs(30)) + .send_get() + .await + .context("Failed to connect to peer for preview")?; if !response.status().is_success() { return Err(anyhow::anyhow!( diff --git a/core/archipelago/src/federation/mod.rs b/core/archipelago/src/federation/mod.rs index 19e4ec4a..dae72565 100644 --- a/core/archipelago/src/federation/mod.rs +++ b/core/archipelago/src/federation/mod.rs @@ -12,6 +12,9 @@ mod types; // Re-export all public items so `crate::federation::*` continues to work. pub use invites::{accept_invite, create_invite}; -pub use storage::{add_node, load_nodes, remove_node, save_nodes, set_trust_level, update_node}; +pub use storage::{ + add_node, fips_npub_for_onion, load_nodes, remove_node, save_nodes, set_trust_level, + update_node, +}; pub use sync::{build_local_state, deploy_to_peer, sync_with_peer}; pub use types::{AppStatus, FederatedNode, NodeStateSnapshot, TrustLevel}; diff --git a/core/archipelago/src/federation/storage.rs b/core/archipelago/src/federation/storage.rs index fe627357..056dbe50 100644 --- a/core/archipelago/src/federation/storage.rs +++ b/core/archipelago/src/federation/storage.rs @@ -47,6 +47,19 @@ pub async fn load_nodes(data_dir: &Path) -> Result> { Ok(file.nodes) } +/// Look up a federated peer's FIPS npub given their onion address. +/// Returns `None` when the onion isn't in our federation list or the +/// peer hasn't advertised a FIPS key. Matching is suffix-tolerant so +/// callers can pass `abc` or `abc.onion` interchangeably. +pub async fn fips_npub_for_onion(data_dir: &Path, onion: &str) -> Option { + let target = onion.trim_end_matches(".onion"); + let nodes = load_nodes(data_dir).await.ok()?; + nodes + .iter() + .find(|n| n.onion.trim_end_matches(".onion") == target) + .and_then(|n| n.fips_npub.clone()) +} + pub async fn save_nodes(data_dir: &Path, nodes: &[FederatedNode]) -> Result<()> { let dir = ensure_dir(data_dir).await?; let file = NodesFile {