fix(mesh): route ContentRef over federation when >160B
mesh.send-content was failing with "Message too large for LoRa: 624
bytes (max 160)" because a single ContentRef envelope (cid + onion +
cap_token + thumb) dwarfs a LoRa frame. Add a federation Tor fallback:
- New POST /archipelago/mesh-typed endpoint accepts
{from_pubkey, typed_envelope_b64, signature}, verifies ed25519 over
the raw wire bytes, and injects the decoded envelope into MeshState
via a new MeshService::inject_typed_from_federation helper. This
shares the same dispatch match as LoRa receives via a new pub(crate)
handle_typed_envelope_direct extracted from handle_typed_message.
- MeshService::send_typed_wire_via_federation POSTs the signed wire to
a peer's onion over TOR_SOCKS_PROXY and records a local Sent record.
- handle_mesh_send_content looks up the peer's onion in federation
storage and routes via federation when available, falling back to
LoRa only when no federation presence is known (still fails on
oversized — chunking is Phase 4).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
649433b7fd
commit
bc3729d99f
@@ -267,6 +267,15 @@ impl ApiHandler {
|
||||
Self::handle_node_message(body_bytes).await
|
||||
}
|
||||
|
||||
// Mesh typed envelope relay over federation — peers POST
|
||||
// pre-encoded TypedEnvelope wire bytes here when the envelope is
|
||||
// too large for a single LoRa frame (primarily ContentRef). No
|
||||
// session auth: the body carries a pubkey + ed25519 signature
|
||||
// over the wire bytes which we verify before dispatching.
|
||||
(Method::POST, "/archipelago/mesh-typed") => {
|
||||
Self::handle_mesh_typed_relay(self.rpc_handler.clone(), body_bytes).await
|
||||
}
|
||||
|
||||
// Blob upload — local/session use only. Session-authenticated so
|
||||
// only the node owner can push attachments into the blob store.
|
||||
(Method::POST, "/api/blob") => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::api::rpc::RpcHandler;
|
||||
use crate::node_message as node_msg;
|
||||
use super::build_response;use anyhow::Result;
|
||||
use hyper::{Response, StatusCode};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::{ApiHandler, is_valid_pubkey_hex, sanitize_html, sanitize_log_string};
|
||||
|
||||
@@ -74,4 +76,85 @@ impl ApiHandler {
|
||||
}
|
||||
Ok(build_response(StatusCode::OK, "application/json", hyper::Body::from(r#"{"ok":true}"#)))
|
||||
}
|
||||
|
||||
/// Federation-routed mesh typed envelope. Body:
|
||||
/// `{from_pubkey, from_name?, typed_envelope_b64, signature}`
|
||||
/// Signature is ed25519 over the raw wire bytes, verified against
|
||||
/// from_pubkey before dispatch.
|
||||
pub(super) async fn handle_mesh_typed_relay(
|
||||
rpc_handler: Arc<RpcHandler>,
|
||||
body: hyper::body::Bytes,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Incoming {
|
||||
from_pubkey: String,
|
||||
#[serde(default)]
|
||||
from_name: Option<String>,
|
||||
typed_envelope_b64: String,
|
||||
signature: String,
|
||||
}
|
||||
let incoming: Incoming = match serde_json::from_slice(&body) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"application/json",
|
||||
hyper::Body::from(format!(r#"{{"error":"bad json: {}"}}"#, e)),
|
||||
));
|
||||
}
|
||||
};
|
||||
if !is_valid_pubkey_hex(&incoming.from_pubkey) {
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"invalid pubkey"}"#),
|
||||
));
|
||||
}
|
||||
let wire = match BASE64.decode(incoming.typed_envelope_b64.as_bytes()) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"bad base64"}"#),
|
||||
));
|
||||
}
|
||||
};
|
||||
match crate::identity::NodeIdentity::verify(&incoming.from_pubkey, &wire, &incoming.signature) {
|
||||
Ok(true) => {}
|
||||
_ => {
|
||||
return Ok(build_response(
|
||||
StatusCode::FORBIDDEN,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"signature rejected"}"#),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Inject into mesh state via the shared MeshService. Mirrors a radio
|
||||
// receive, so the message lands in the same chat stream as LoRa-
|
||||
// delivered messages from the same peer.
|
||||
let service = rpc_handler.mesh_service_arc();
|
||||
let svc_guard = service.read().await;
|
||||
let Some(svc) = svc_guard.as_ref() else {
|
||||
return Ok(build_response(
|
||||
StatusCode::SERVICE_UNAVAILABLE,
|
||||
"application/json",
|
||||
hyper::Body::from(r#"{"error":"mesh not running"}"#),
|
||||
));
|
||||
};
|
||||
if let Err(e) = svc
|
||||
.inject_typed_from_federation(&incoming.from_pubkey, incoming.from_name.as_deref(), wire)
|
||||
.await
|
||||
{
|
||||
tracing::warn!("mesh-typed relay inject failed: {}", e);
|
||||
return Ok(build_response(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"application/json",
|
||||
hyper::Body::from(format!(r#"{{"error":"{}"}}"#, e)),
|
||||
));
|
||||
}
|
||||
Ok(build_response(StatusCode::OK, "application/json", hyper::Body::from(r#"{"ok":true}"#)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user