route indeehub catalog through node FIPS
This commit is contained in:
@@ -24,7 +24,7 @@ use crate::container::{ContainerOrchestrator, DevContainerOrchestrator};
|
||||
use crate::monitoring::MetricsStore;
|
||||
use crate::session::{self, SessionStore};
|
||||
use crate::state::StateManager;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use hyper::{Method, Request, Response, StatusCode};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::sync::Arc;
|
||||
@@ -225,6 +225,50 @@ impl ApiHandler {
|
||||
))
|
||||
}
|
||||
|
||||
/// Serve IndeeHub's sanitized public project catalog on the FIPS peer
|
||||
/// listener. The app container remains on loopback; peers never receive
|
||||
/// its session-gated port directly.
|
||||
async fn handle_indeehub_public_catalog(&self) -> Result<Response<hyper::Body>> {
|
||||
let response = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()?
|
||||
.get("http://127.0.0.1:7778/api/projects/public-catalog")
|
||||
.header(reqwest::header::ACCEPT, "application/json")
|
||||
.send()
|
||||
.await;
|
||||
let response = match response {
|
||||
Ok(response) => response,
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "IndeeHub public catalog loopback fetch failed");
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"IndeeHub catalog unavailable"}"#),
|
||||
));
|
||||
}
|
||||
};
|
||||
if !response.status().is_success() {
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_GATEWAY,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"IndeeHub catalog unavailable"}"#),
|
||||
));
|
||||
}
|
||||
let bytes = response.bytes().await?;
|
||||
anyhow::ensure!(
|
||||
bytes.len() <= 8 * 1024 * 1024,
|
||||
"IndeeHub catalog exceeds limit"
|
||||
);
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_slice(&bytes).context("IndeeHub catalog is not valid JSON")?;
|
||||
anyhow::ensure!(value.is_array(), "IndeeHub catalog must be an array");
|
||||
Ok(build_response(
|
||||
StatusCode::OK,
|
||||
"application/json",
|
||||
hyper::Body::from(bytes),
|
||||
))
|
||||
}
|
||||
|
||||
/// Serve an encrypted backup archive (`<data_dir>/backups/<id>.bak`) as a
|
||||
/// browser download. The archive is passphrase-encrypted at rest; the
|
||||
/// session gate at the route controls who can fetch it.
|
||||
@@ -795,6 +839,12 @@ impl ApiHandler {
|
||||
.body(hyper::Body::from(body))?)
|
||||
}
|
||||
|
||||
// Public IndeeHub metadata is also exposed on the FIPS peer
|
||||
// listener, whose path filter is the only ingress gate here.
|
||||
(Method::GET, "/api/public-catalog/indeedhub") => {
|
||||
self.handle_indeehub_public_catalog().await
|
||||
}
|
||||
|
||||
(Method::GET, "/api/app-catalog") => {
|
||||
if !self.is_authenticated(&headers).await {
|
||||
return Ok(Self::unauthorized());
|
||||
|
||||
@@ -1418,6 +1418,76 @@ impl RpcHandler {
|
||||
}))
|
||||
}
|
||||
|
||||
/// Browse IndeeHub's public film catalogs through the native peer bridge.
|
||||
/// The bridge selects FIPS first (with the normal safe fallback policy),
|
||||
/// so the browser never needs a peer LAN address or an onion connection.
|
||||
pub(super) async fn handle_content_browse_indeehub_peers(&self) -> Result<serde_json::Value> {
|
||||
let nodes = crate::federation::load_nodes(&self.config.data_dir)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let peers: Vec<_> = nodes
|
||||
.into_iter()
|
||||
.filter(|node| is_valid_v3_onion(&node.onion))
|
||||
.collect();
|
||||
let calls = peers.into_iter().map(|node| async move {
|
||||
let onion = node.onion.clone();
|
||||
let request_onion = onion.clone();
|
||||
let fips = node.fips_npub.clone();
|
||||
let result = tokio::time::timeout(std::time::Duration::from_secs(12), async move {
|
||||
let (response, transport) = crate::fips::dial::PeerRequest::new(
|
||||
fips.as_deref(),
|
||||
&request_onion,
|
||||
"/api/public-catalog/indeedhub",
|
||||
)
|
||||
.service(crate::settings::transport::PeerService::PeerFiles)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.fips_timeout(std::time::Duration::from_secs(6))
|
||||
.send_get()
|
||||
.await?;
|
||||
if !response.status().is_success() {
|
||||
anyhow::bail!("peer returned {}", response.status());
|
||||
}
|
||||
let listings: serde_json::Value = response
|
||||
.json()
|
||||
.await
|
||||
.context("invalid IndeeHub peer catalog")?;
|
||||
Ok::<_, anyhow::Error>((listings, transport))
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok);
|
||||
(onion, result)
|
||||
});
|
||||
let mut items = Vec::new();
|
||||
let mut reached = 0usize;
|
||||
for (onion, result) in futures_util::future::join_all(calls).await {
|
||||
let Some((listings, transport)) = result else {
|
||||
continue;
|
||||
};
|
||||
let Some(listings) = listings.as_array() else {
|
||||
continue;
|
||||
};
|
||||
reached += 1;
|
||||
for listing in listings {
|
||||
let Some(mut listing) = listing.as_object().cloned() else {
|
||||
continue;
|
||||
};
|
||||
listing.insert("peer".to_string(), serde_json::json!(onion));
|
||||
listing.insert(
|
||||
"transport".to_string(),
|
||||
serde_json::json!(transport.to_string()),
|
||||
);
|
||||
items.push(serde_json::Value::Object(listing));
|
||||
}
|
||||
}
|
||||
Ok(serde_json::json!({
|
||||
"items": items,
|
||||
"peers_reached": reached,
|
||||
"peers_total": reached,
|
||||
"partial": false,
|
||||
}))
|
||||
}
|
||||
|
||||
/// `content.owned-get` — return a purchased item's bytes (base64) from the
|
||||
/// local cache for in-app viewing/saving. No network, no re-payment.
|
||||
pub(super) async fn handle_content_owned_get(
|
||||
|
||||
@@ -354,6 +354,7 @@ impl RpcHandler {
|
||||
"content.download-peer-paid" => self.handle_content_download_peer_paid(params).await,
|
||||
"content.indeehub-projects" => self.handle_content_indeehub_projects().await,
|
||||
"content.browse-all-peers" => self.handle_content_browse_all_peers().await,
|
||||
"content.browse-indeehub-peers" => self.handle_content_browse_indeehub_peers().await,
|
||||
"content.playback-handle" => self.handle_playback_handle(params, session_token).await,
|
||||
"content.playback-prepare" => self.handle_playback_prepare(params, session_token).await,
|
||||
"content.playback-start" => self.handle_playback_start(params, session_token).await,
|
||||
|
||||
@@ -1468,6 +1468,9 @@ pub fn is_peer_allowed_path(path: &str) -> bool {
|
||||
| "/archipelago/mesh-typed"
|
||||
| "/dwn"
|
||||
| "/transport/inbox"
|
||||
// IndeeHub public metadata is loopback-fetched by the native
|
||||
// handler; the app port itself is never exposed on FIPS.
|
||||
| "/api/public-catalog/indeedhub"
|
||||
// Content *catalog* — the peer-browse entry point. This is the
|
||||
// exact path `/content` (no trailing slash); the prefix match
|
||||
// below only covers `/content/<id>` item fetches, so without
|
||||
|
||||
Reference in New Issue
Block a user