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::monitoring::MetricsStore;
|
||||||
use crate::session::{self, SessionStore};
|
use crate::session::{self, SessionStore};
|
||||||
use crate::state::StateManager;
|
use crate::state::StateManager;
|
||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use hyper::{Method, Request, Response, StatusCode};
|
use hyper::{Method, Request, Response, StatusCode};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::sync::Arc;
|
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
|
/// Serve an encrypted backup archive (`<data_dir>/backups/<id>.bak`) as a
|
||||||
/// browser download. The archive is passphrase-encrypted at rest; the
|
/// browser download. The archive is passphrase-encrypted at rest; the
|
||||||
/// session gate at the route controls who can fetch it.
|
/// session gate at the route controls who can fetch it.
|
||||||
@@ -795,6 +839,12 @@ impl ApiHandler {
|
|||||||
.body(hyper::Body::from(body))?)
|
.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") => {
|
(Method::GET, "/api/app-catalog") => {
|
||||||
if !self.is_authenticated(&headers).await {
|
if !self.is_authenticated(&headers).await {
|
||||||
return Ok(Self::unauthorized());
|
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
|
/// `content.owned-get` — return a purchased item's bytes (base64) from the
|
||||||
/// local cache for in-app viewing/saving. No network, no re-payment.
|
/// local cache for in-app viewing/saving. No network, no re-payment.
|
||||||
pub(super) async fn handle_content_owned_get(
|
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.download-peer-paid" => self.handle_content_download_peer_paid(params).await,
|
||||||
"content.indeehub-projects" => self.handle_content_indeehub_projects().await,
|
"content.indeehub-projects" => self.handle_content_indeehub_projects().await,
|
||||||
"content.browse-all-peers" => self.handle_content_browse_all_peers().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-handle" => self.handle_playback_handle(params, session_token).await,
|
||||||
"content.playback-prepare" => self.handle_playback_prepare(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,
|
"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"
|
| "/archipelago/mesh-typed"
|
||||||
| "/dwn"
|
| "/dwn"
|
||||||
| "/transport/inbox"
|
| "/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
|
// Content *catalog* — the peer-browse entry point. This is the
|
||||||
// exact path `/content` (no trailing slash); the prefix match
|
// exact path `/content` (no trailing slash); the prefix match
|
||||||
// below only covers `/content/<id>` item fetches, so without
|
// below only covers `/content/<id>` item fetches, so without
|
||||||
|
|||||||
Reference in New Issue
Block a user