feat: Lightning channel backup, Web5 mobile tab active, file path fix

Task 14: Lightning Channel Backup
- New lnd.export-channel-backup RPC — exports SCB (Static Channel Backup)
- Settings UI: "Lightning Channel Backup" section with export + copy
- Returns base64 backup data, channel count, timestamp

Web5 mobile tab active state
- Fixed combined tab matching for Web5: includes /web5, /federation, /mesh routes
- Previously only matched /cloud and /server (wrong branch)

Content file path fix
- Allow forward slashes in filenames for subdirectories (Music/song.mp3)
- Still block .., \, null bytes, hidden files, absolute paths

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-19 21:47:18 +00:00
parent 1f732d8d08
commit c5417640a2
4 changed files with 105 additions and 1 deletions

View File

@@ -938,6 +938,53 @@ impl RpcHandler {
"grpc_port": 10009,
}))
}
/// lnd.export-channel-backup — Export all channel static backups (SCB).
/// Returns base64-encoded multi-channel backup that can restore channels on a new node.
pub(super) async fn handle_lnd_export_channel_backup(&self) -> Result<serde_json::Value> {
let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
let macaroon_bytes = tokio::fs::read(macaroon_path)
.await
.context("Failed to read LND admin macaroon")?;
let macaroon_hex = hex::encode(&macaroon_bytes);
let cert_path = "/var/lib/archipelago/lnd/tls.cert";
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()
.context("Failed to build HTTP client")?;
let resp = client
.get("https://127.0.0.1:8080/v1/channels/backup")
.header("Grpc-Metadata-macaroon", &macaroon_hex)
.send()
.await
.context("Failed to reach LND REST API")?;
if !resp.status().is_success() {
anyhow::bail!("LND returned {}", resp.status());
}
let data: serde_json::Value = resp.json().await.context("Invalid JSON from LND")?;
// Extract the multi_chan_backup bytes
let backup_b64 = data
.get("multi_chan_backup")
.and_then(|m| m.get("multi_chan_backup"))
.and_then(|b| b.as_str())
.unwrap_or("");
Ok(serde_json::json!({
"backup": backup_b64,
"channel_count": data.get("multi_chan_backup")
.and_then(|m| m.get("chan_points"))
.and_then(|c| c.as_array())
.map(|a| a.len())
.unwrap_or(0),
"timestamp": chrono::Utc::now().to_rfc3339(),
}))
}
}
// Channel types

View File

@@ -514,6 +514,7 @@ impl RpcHandler {
"lnd.create-raw-tx" => self.handle_lnd_create_raw_tx(params).await,
"lnd.gettransactions" => self.handle_lnd_gettransactions().await,
"lnd.connect-info" => self.handle_lnd_connect_info().await,
"lnd.export-channel-backup" => self.handle_lnd_export_channel_backup().await,
// Multi-identity management
"identity.list" => self.handle_identity_list(params).await,