feat(content): route peer content fetch via FIPS first
All four content-over-peer handlers prefer FIPS when the peer is in our federation and has advertised a FIPS npub; fall back to Tor otherwise (unknown peers, FIPS daemon down, transient failure). - content.handle_content_download_peer / _paid: DID-authenticated fetch, payment token header threaded through both transports. - content.handle_content_browse_peer / _preview: no DID header by design (anonymous browse) — still benefits from FIPS when the peer happens to be federated. - federation::fips_npub_for_onion: storage helper that looks up a peer's FIPS npub from the federation nodes file given their onion address. Suffix-tolerant (`abc` matches `abc.onion`). Preserves the Tor-only path for truly unknown peers: PeerRequest returns Err from the Tor branch instead of silently succeeding, matching the previous behavior when the peer was unreachable. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<serde_json::Value>,
|
||||
@@ -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<serde_json::Value>,
|
||||
@@ -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!(
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -47,6 +47,19 @@ pub async fn load_nodes(data_dir: &Path) -> Result<Vec<FederatedNode>> {
|
||||
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<String> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user