backup commit
This commit is contained in:
@@ -51,6 +51,11 @@ impl ApiHandler {
|
||||
})
|
||||
}
|
||||
|
||||
/// Access the RPC handler (for service initialization after construction).
|
||||
pub fn rpc_handler(&self) -> &Arc<RpcHandler> {
|
||||
&self.rpc_handler
|
||||
}
|
||||
|
||||
/// Check if the request has a valid session cookie.
|
||||
async fn is_authenticated(&self, headers: &hyper::HeaderMap) -> bool {
|
||||
match session::extract_session_cookie(headers) {
|
||||
@@ -161,6 +166,11 @@ impl ApiHandler {
|
||||
// Electrs status — unauthenticated (read-only sync status)
|
||||
(Method::GET, "/electrs-status") => Self::handle_electrs_status().await,
|
||||
|
||||
// LND connect info — unauthenticated (read-only, localhost only)
|
||||
(Method::GET, "/lnd-connect-info") => {
|
||||
Self::handle_lnd_connect_info(self.rpc_handler.clone()).await
|
||||
}
|
||||
|
||||
// Container logs — requires session
|
||||
(Method::GET, path) if path.starts_with("/api/container/logs") => {
|
||||
if !self.is_authenticated(&headers).await {
|
||||
@@ -307,6 +317,28 @@ impl ApiHandler {
|
||||
.unwrap())
|
||||
}
|
||||
|
||||
async fn handle_lnd_connect_info(
|
||||
rpc: std::sync::Arc<super::rpc::RpcHandler>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
match rpc.handle_lnd_connect_info().await {
|
||||
Ok(val) => {
|
||||
let body = serde_json::to_vec(&val).unwrap_or_default();
|
||||
Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(body))
|
||||
.unwrap())
|
||||
}
|
||||
Err(e) => Ok(Response::builder()
|
||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(hyper::Body::from(
|
||||
serde_json::json!({"error": e.to_string()}).to_string(),
|
||||
))
|
||||
.unwrap()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_lnd_proxy(path: &str, cors_origin: &str) -> Result<Response<hyper::Body>> {
|
||||
let suffix = path.strip_prefix("/proxy/lnd").unwrap_or("/");
|
||||
let url = format!("http://127.0.0.1:8080{}", suffix);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::RpcHandler;
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
|
||||
@@ -797,6 +798,50 @@ impl RpcHandler {
|
||||
"incoming_pending_count": incoming_pending,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return LND connection info: base64url-encoded TLS cert and admin macaroon
|
||||
/// for building lndconnect:// URIs in the frontend.
|
||||
pub(crate) async fn handle_lnd_connect_info(&self) -> Result<serde_json::Value> {
|
||||
let cert_path = "/var/lib/archipelago/lnd/tls.cert";
|
||||
let macaroon_path =
|
||||
"/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
|
||||
|
||||
// Read and encode TLS cert (PEM → DER → base64url)
|
||||
let cert_pem = tokio::fs::read_to_string(cert_path)
|
||||
.await
|
||||
.context("Failed to read LND TLS certificate")?;
|
||||
let cert_der_b64: String = cert_pem
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with("-----"))
|
||||
.collect();
|
||||
let cert_der = base64::engine::general_purpose::STANDARD
|
||||
.decode(&cert_der_b64)
|
||||
.context("Failed to decode PEM base64")?;
|
||||
let cert_b64url = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cert_der);
|
||||
|
||||
// Read and encode macaroon (binary → base64url)
|
||||
let macaroon_bytes = tokio::fs::read(macaroon_path)
|
||||
.await
|
||||
.context("Failed to read LND admin macaroon")?;
|
||||
let macaroon_b64url =
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&macaroon_bytes);
|
||||
|
||||
// Read Tor onion address if available
|
||||
let tor_onion = tokio::fs::read_to_string(
|
||||
"/var/lib/archipelago/tor/hidden_service_lnd/hostname",
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(|s| s.trim().to_string());
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"cert_base64url": cert_b64url,
|
||||
"macaroon_base64url": macaroon_b64url,
|
||||
"tor_onion": tor_onion,
|
||||
"rest_port": 8080,
|
||||
"grpc_port": 10009,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Channel types
|
||||
|
||||
@@ -1,58 +1,122 @@
|
||||
use super::RpcHandler;
|
||||
use crate::{identity, mesh};
|
||||
use crate::mesh;
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
impl RpcHandler {
|
||||
/// mesh.status — Get mesh radio status and detected devices.
|
||||
/// mesh.status — Get mesh radio status, device info, and peer count.
|
||||
pub(super) async fn handle_mesh_status(&self) -> Result<serde_json::Value> {
|
||||
let config = mesh::load_config(&self.config.data_dir).await?;
|
||||
let devices = mesh::detect_meshtastic_devices().await;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"enabled": config.enabled,
|
||||
"device_path": config.device_path,
|
||||
"channel_name": config.channel_name,
|
||||
"broadcast_identity": config.broadcast_identity,
|
||||
"detected_devices": devices,
|
||||
}))
|
||||
let service = self.mesh_service.read().await;
|
||||
if let Some(svc) = service.as_ref() {
|
||||
let status = svc.status().await;
|
||||
Ok(serde_json::to_value(status)?)
|
||||
} else {
|
||||
// No service running — return basic config + device detection
|
||||
let config = mesh::load_config(&self.config.data_dir).await?;
|
||||
let devices = mesh::detect_devices().await;
|
||||
Ok(serde_json::json!({
|
||||
"enabled": config.enabled,
|
||||
"device_connected": false,
|
||||
"device_type": "unknown",
|
||||
"device_path": config.device_path,
|
||||
"channel_name": config.channel_name.unwrap_or_else(|| "archipelago".to_string()),
|
||||
"detected_devices": devices,
|
||||
"peer_count": 0,
|
||||
"messages_sent": 0,
|
||||
"messages_received": 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// mesh.discover — Discover nodes via mesh radio.
|
||||
pub(super) async fn handle_mesh_discover(
|
||||
/// mesh.peers — List discovered mesh peers.
|
||||
pub(super) async fn handle_mesh_peers(&self) -> Result<serde_json::Value> {
|
||||
let service = self.mesh_service.read().await;
|
||||
if let Some(svc) = service.as_ref() {
|
||||
let peers = svc.peers().await;
|
||||
Ok(serde_json::json!({
|
||||
"peers": peers,
|
||||
"count": peers.len(),
|
||||
}))
|
||||
} else {
|
||||
Ok(serde_json::json!({
|
||||
"peers": [],
|
||||
"count": 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// mesh.messages — Get recent mesh message history.
|
||||
pub(super) async fn handle_mesh_messages(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let device_path = params
|
||||
let limit = params
|
||||
.as_ref()
|
||||
.and_then(|p| p.get("device_path"))
|
||||
.and_then(|v| v.as_str());
|
||||
.and_then(|p| p.get("limit"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.map(|n| n as usize);
|
||||
|
||||
let config = mesh::load_config(&self.config.data_dir).await?;
|
||||
let effective_device = device_path.or(config.device_path.as_deref());
|
||||
let service = self.mesh_service.read().await;
|
||||
if let Some(svc) = service.as_ref() {
|
||||
let messages = svc.messages(limit).await;
|
||||
Ok(serde_json::json!({
|
||||
"messages": messages,
|
||||
"count": messages.len(),
|
||||
}))
|
||||
} else {
|
||||
Ok(serde_json::json!({
|
||||
"messages": [],
|
||||
"count": 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = mesh::discover_nodes(effective_device).await?;
|
||||
/// mesh.send — Send an encrypted message to a mesh peer.
|
||||
pub(super) async fn handle_mesh_send(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
|
||||
let contact_id = params
|
||||
.get("contact_id")
|
||||
.and_then(|v| v.as_u64())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing contact_id"))? as u32;
|
||||
|
||||
let message = params
|
||||
.get("message")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing message"))?;
|
||||
|
||||
if message.is_empty() {
|
||||
anyhow::bail!("Message cannot be empty");
|
||||
}
|
||||
|
||||
let service = self.mesh_service.read().await;
|
||||
let svc = service
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Mesh service not running. Enable mesh first."))?;
|
||||
|
||||
let msg = svc.send_message(contact_id, message).await?;
|
||||
info!(contact_id, encrypted = msg.encrypted, "Sent mesh message");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"nodes": nodes,
|
||||
"count": nodes.len(),
|
||||
"sent": true,
|
||||
"message_id": msg.id,
|
||||
"encrypted": msg.encrypted,
|
||||
}))
|
||||
}
|
||||
|
||||
/// mesh.broadcast — Broadcast our node identity over mesh.
|
||||
pub(super) async fn handle_mesh_broadcast(&self) -> Result<serde_json::Value> {
|
||||
let config = mesh::load_config(&self.config.data_dir).await?;
|
||||
if !config.enabled {
|
||||
anyhow::bail!("Mesh networking is not enabled. Configure it first.");
|
||||
}
|
||||
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let did = identity::did_key_from_pubkey_hex(&data.server_info.pubkey)?;
|
||||
let pubkey = &data.server_info.pubkey;
|
||||
|
||||
mesh::broadcast_identity(&did, pubkey, config.device_path.as_deref()).await?;
|
||||
let service = self.mesh_service.read().await;
|
||||
let svc = service
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Mesh service not running. Enable mesh first."))?;
|
||||
|
||||
svc.broadcast_identity().await?;
|
||||
info!("Broadcast identity over mesh");
|
||||
|
||||
Ok(serde_json::json!({ "broadcast": true }))
|
||||
}
|
||||
|
||||
@@ -77,9 +141,18 @@ impl RpcHandler {
|
||||
if let Some(broadcast) = params.get("broadcast_identity").and_then(|v| v.as_bool()) {
|
||||
config.broadcast_identity = broadcast;
|
||||
}
|
||||
if let Some(name) = params.get("advert_name").and_then(|v| v.as_str()) {
|
||||
config.advert_name = Some(name.to_string());
|
||||
}
|
||||
|
||||
mesh::save_config(&self.config.data_dir, &config).await?;
|
||||
|
||||
// If we have a running service, update its config
|
||||
let mut service = self.mesh_service.write().await;
|
||||
if let Some(svc) = service.as_mut() {
|
||||
svc.configure(config.clone()).await?;
|
||||
}
|
||||
|
||||
info!("Mesh config updated");
|
||||
Ok(serde_json::json!({
|
||||
"configured": true,
|
||||
|
||||
@@ -23,6 +23,7 @@ mod peers;
|
||||
mod router;
|
||||
mod security;
|
||||
mod tor;
|
||||
mod transport;
|
||||
mod totp;
|
||||
mod system;
|
||||
mod update;
|
||||
@@ -166,6 +167,8 @@ pub struct RpcHandler {
|
||||
login_rate_limiter: LoginRateLimiter,
|
||||
endpoint_rate_limiter: EndpointRateLimiter,
|
||||
response_cache: ResponseCache,
|
||||
mesh_service: Arc<tokio::sync::RwLock<Option<crate::mesh::MeshService>>>,
|
||||
transport_router: Arc<tokio::sync::RwLock<Option<Arc<crate::transport::TransportRouter>>>>,
|
||||
}
|
||||
|
||||
impl RpcHandler {
|
||||
@@ -196,9 +199,26 @@ impl RpcHandler {
|
||||
login_rate_limiter: LoginRateLimiter::new(),
|
||||
endpoint_rate_limiter: EndpointRateLimiter::new(),
|
||||
response_cache: ResponseCache::new(5),
|
||||
mesh_service: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
transport_router: Arc::new(tokio::sync::RwLock::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Set the mesh service (called after identity is loaded).
|
||||
pub async fn set_mesh_service(&self, service: crate::mesh::MeshService) {
|
||||
*self.mesh_service.write().await = Some(service);
|
||||
}
|
||||
|
||||
/// Set the transport router (called after all transports are initialized).
|
||||
pub async fn set_transport_router(&self, router: Arc<crate::transport::TransportRouter>) {
|
||||
*self.transport_router.write().await = Some(router);
|
||||
}
|
||||
|
||||
/// Get reference to the mesh service Arc (for MeshTransport wrapper).
|
||||
pub fn mesh_service_arc(&self) -> Arc<tokio::sync::RwLock<Option<crate::mesh::MeshService>>> {
|
||||
Arc::clone(&self.mesh_service)
|
||||
}
|
||||
|
||||
fn cookie_suffix(&self) -> &'static str {
|
||||
if self.config.dev_mode { "" } else { "; Secure" }
|
||||
}
|
||||
@@ -471,6 +491,7 @@ impl RpcHandler {
|
||||
"lnd.create-psbt" => self.handle_lnd_create_psbt(params).await,
|
||||
"lnd.finalize-psbt" => self.handle_lnd_finalize_psbt(params).await,
|
||||
"lnd.gettransactions" => self.handle_lnd_gettransactions().await,
|
||||
"lnd.connect-info" => self.handle_lnd_connect_info().await,
|
||||
|
||||
// Multi-identity management
|
||||
"identity.list" => self.handle_identity_list(params).await,
|
||||
@@ -618,12 +639,20 @@ impl RpcHandler {
|
||||
"marketplace.create-invoice" => self.handle_marketplace_create_invoice(params).await,
|
||||
"marketplace.check-payment" => self.handle_marketplace_check_payment(params).await,
|
||||
|
||||
// Mesh networking
|
||||
// Mesh networking (Meshcore LoRa)
|
||||
"mesh.status" => self.handle_mesh_status().await,
|
||||
"mesh.discover" => self.handle_mesh_discover(params).await,
|
||||
"mesh.peers" => self.handle_mesh_peers().await,
|
||||
"mesh.messages" => self.handle_mesh_messages(params).await,
|
||||
"mesh.send" => self.handle_mesh_send(params).await,
|
||||
"mesh.broadcast" => self.handle_mesh_broadcast().await,
|
||||
"mesh.configure" => self.handle_mesh_configure(params).await,
|
||||
|
||||
// Transport layer (unified routing)
|
||||
"transport.status" => self.handle_transport_status().await,
|
||||
"transport.peers" => self.handle_transport_peers().await,
|
||||
"transport.send" => self.handle_transport_send(params).await,
|
||||
"transport.set-mode" => self.handle_transport_set_mode(params).await,
|
||||
|
||||
// Server settings
|
||||
"server.set-name" => self.handle_server_set_name(params).await,
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use super::RpcHandler;
|
||||
use crate::transport::{MessageType, TransportMessage};
|
||||
use anyhow::Result;
|
||||
use tracing::info;
|
||||
|
||||
impl RpcHandler {
|
||||
/// transport.status — Get available transports and their status.
|
||||
pub(super) async fn handle_transport_status(&self) -> Result<serde_json::Value> {
|
||||
let router = self.transport_router.read().await;
|
||||
if let Some(r) = router.as_ref() {
|
||||
let transports: Vec<serde_json::Value> = r
|
||||
.transport_status()
|
||||
.into_iter()
|
||||
.map(|(kind, available)| {
|
||||
serde_json::json!({
|
||||
"kind": kind.to_string(),
|
||||
"available": available,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let peer_count = r.registry.count().await;
|
||||
let mesh_only = r.is_mesh_only().await;
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"transports": transports,
|
||||
"mesh_only": mesh_only,
|
||||
"peer_count": peer_count,
|
||||
}))
|
||||
} else {
|
||||
Ok(serde_json::json!({
|
||||
"transports": [],
|
||||
"mesh_only": false,
|
||||
"peer_count": 0,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// transport.peers — Get unified peer list with per-peer transport capabilities.
|
||||
pub(super) async fn handle_transport_peers(&self) -> Result<serde_json::Value> {
|
||||
let router = self.transport_router.read().await;
|
||||
if let Some(r) = router.as_ref() {
|
||||
let peers = r.registry.all_peers().await;
|
||||
let peer_values: Vec<serde_json::Value> = peers
|
||||
.into_iter()
|
||||
.map(|p| {
|
||||
let available = p.available_transports();
|
||||
let preferred = available.first().map(|t| t.to_string());
|
||||
serde_json::json!({
|
||||
"did": p.did,
|
||||
"pubkey_hex": p.pubkey_hex,
|
||||
"name": p.name,
|
||||
"trust_level": p.trust_level,
|
||||
"mesh_contact_id": p.mesh_contact_id,
|
||||
"lan_address": p.lan_address,
|
||||
"onion_address": p.onion_address,
|
||||
"preferred_transport": preferred,
|
||||
"available_transports": available.iter().map(|t| t.to_string()).collect::<Vec<_>>(),
|
||||
"last_seen": p.last_mesh.or(p.last_lan).or(p.last_tor),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::json!({ "peers": peer_values }))
|
||||
} else {
|
||||
Ok(serde_json::json!({ "peers": [] }))
|
||||
}
|
||||
}
|
||||
|
||||
/// transport.send — Send a message to a peer via best available transport.
|
||||
pub(super) async fn handle_transport_send(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.as_ref().ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let did = params["did"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'did' param"))?
|
||||
.to_string();
|
||||
let payload = params["payload"]
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'payload' param"))?
|
||||
.to_string();
|
||||
|
||||
let router = self.transport_router.read().await;
|
||||
let router = router
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Transport router not initialized"))?;
|
||||
|
||||
let (data, _) = self.state_manager.get_snapshot().await;
|
||||
let our_did = crate::identity::did_key_from_pubkey_hex(&data.server_info.pubkey)
|
||||
.unwrap_or_default();
|
||||
|
||||
let message = TransportMessage {
|
||||
from_did: our_did,
|
||||
payload: payload.as_bytes().to_vec(),
|
||||
message_type: MessageType::PeerMessage,
|
||||
};
|
||||
|
||||
let transport_used = router.send_to_peer(did, &message).await?;
|
||||
|
||||
info!(did = %did, transport = %transport_used, "Sent message via transport");
|
||||
Ok(serde_json::json!({
|
||||
"sent": true,
|
||||
"transport_used": transport_used.to_string(),
|
||||
"did": did,
|
||||
}))
|
||||
}
|
||||
|
||||
/// transport.set-mode — Toggle mesh-only (off-grid) mode.
|
||||
pub(super) async fn handle_transport_set_mode(
|
||||
&self,
|
||||
params: Option<serde_json::Value>,
|
||||
) -> Result<serde_json::Value> {
|
||||
let params = params.as_ref().ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
||||
let mesh_only = params["mesh_only"]
|
||||
.as_bool()
|
||||
.ok_or_else(|| anyhow::anyhow!("Missing 'mesh_only' bool param"))?;
|
||||
|
||||
let router = self.transport_router.read().await;
|
||||
let router = router
|
||||
.as_ref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Transport router not initialized"))?;
|
||||
|
||||
router.set_mesh_only(mesh_only).await;
|
||||
|
||||
// Also persist to mesh config
|
||||
let mut mesh_config = crate::mesh::load_config(&self.config.data_dir)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
mesh_config.mesh_only_mode = Some(mesh_only);
|
||||
crate::mesh::save_config(&self.config.data_dir, &mesh_config).await?;
|
||||
|
||||
info!(mesh_only = mesh_only, "Transport mode updated");
|
||||
Ok(serde_json::json!({
|
||||
"mesh_only": mesh_only,
|
||||
"configured": true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user