2026-03-11 12:55:13 +00:00
|
|
|
use super::RpcHandler;
|
|
|
|
|
use crate::webhooks;
|
|
|
|
|
use anyhow::Result;
|
|
|
|
|
use tracing::info;
|
|
|
|
|
|
|
|
|
|
impl RpcHandler {
|
|
|
|
|
/// webhook.get-config — Get current webhook configuration.
|
|
|
|
|
pub(super) async fn handle_webhook_get_config(&self) -> Result<serde_json::Value> {
|
|
|
|
|
let config = webhooks::load_config(&self.config.data_dir).await?;
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"enabled": config.enabled,
|
|
|
|
|
"url": config.url,
|
|
|
|
|
"events": config.events,
|
|
|
|
|
"has_secret": config.secret.is_some(),
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// webhook.configure — Update webhook configuration.
|
|
|
|
|
pub(super) async fn handle_webhook_configure(
|
|
|
|
|
&self,
|
|
|
|
|
params: Option<serde_json::Value>,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?;
|
|
|
|
|
|
|
|
|
|
let mut config = webhooks::load_config(&self.config.data_dir).await?;
|
|
|
|
|
|
|
|
|
|
if let Some(enabled) = params.get("enabled").and_then(|v| v.as_bool()) {
|
|
|
|
|
config.enabled = enabled;
|
|
|
|
|
}
|
|
|
|
|
if let Some(url) = params.get("url").and_then(|v| v.as_str()) {
|
fix: harden input validation across all RPC endpoints (PENTEST-02)
Manual security audit of 130+ RPC endpoints. Critical fixes:
- LND: validate pubkey (66-char hex), Bitcoin addresses, channel points,
amount bounds, payment request format, memo length, peer address
- Package: validate_app_id on start/stop/restart/bundled-app handlers,
validate volume host paths (must be under /var/lib/archipelago/),
validate Docker image in bundled-app-start
- Container: validate_app_id on all 6 handlers, canonicalize manifest paths
- Network: path traversal prevention in connection request deletion
- Backup: backup ID validation in delete handler
- Webhooks: URL scheme validation, SSRF prevention for private IPs
- Security: validate app_id in secret rotation
- Interfaces: WiFi password length/null validation, strict IP/gateway/DNS
parsing for static ethernet config
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 14:32:49 +00:00
|
|
|
// Validate webhook URL scheme and reject obviously dangerous targets
|
|
|
|
|
if !url.is_empty() {
|
|
|
|
|
if !url.starts_with("https://") && !url.starts_with("http://") {
|
|
|
|
|
anyhow::bail!("Webhook URL must use HTTP(S)");
|
|
|
|
|
}
|
|
|
|
|
if !self.config.dev_mode && !url.starts_with("https://") {
|
|
|
|
|
anyhow::bail!("Webhook URL must use HTTPS in production");
|
|
|
|
|
}
|
|
|
|
|
// Extract host portion and reject private/internal addresses
|
|
|
|
|
let host_part = url
|
|
|
|
|
.trim_start_matches("https://")
|
|
|
|
|
.trim_start_matches("http://")
|
|
|
|
|
.split('/')
|
|
|
|
|
.next()
|
|
|
|
|
.unwrap_or("")
|
|
|
|
|
.split(':')
|
|
|
|
|
.next()
|
|
|
|
|
.unwrap_or("");
|
|
|
|
|
let is_private = host_part == "localhost"
|
|
|
|
|
|| host_part == "127.0.0.1"
|
|
|
|
|
|| host_part == "::1"
|
|
|
|
|
|| host_part.starts_with("10.")
|
|
|
|
|
|| host_part.starts_with("172.")
|
|
|
|
|
|| host_part.starts_with("192.168.")
|
|
|
|
|
|| host_part.starts_with("169.254.");
|
|
|
|
|
if is_private && !self.config.dev_mode {
|
|
|
|
|
anyhow::bail!("Webhook URL must not point to private/local addresses");
|
|
|
|
|
}
|
|
|
|
|
if url.len() > 2048 {
|
|
|
|
|
anyhow::bail!("Webhook URL too long");
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-11 12:55:13 +00:00
|
|
|
config.url = url.to_string();
|
|
|
|
|
}
|
|
|
|
|
if let Some(secret) = params.get("secret").and_then(|v| v.as_str()) {
|
|
|
|
|
config.secret = if secret.is_empty() {
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(secret.to_string())
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
if let Some(events) = params.get("events") {
|
|
|
|
|
if let Ok(parsed) = serde_json::from_value::<Vec<webhooks::WebhookEvent>>(events.clone())
|
|
|
|
|
{
|
|
|
|
|
config.events = parsed;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
webhooks::save_config(&self.config.data_dir, &config).await?;
|
|
|
|
|
info!("Webhook config updated: enabled={}, url={}", config.enabled, config.url);
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"configured": true,
|
|
|
|
|
"enabled": config.enabled,
|
|
|
|
|
"url": config.url,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// webhook.test — Send a test webhook notification.
|
|
|
|
|
pub(super) async fn handle_webhook_test(&self) -> Result<serde_json::Value> {
|
|
|
|
|
let config = webhooks::load_config(&self.config.data_dir).await?;
|
|
|
|
|
if !config.enabled || config.url.is_empty() {
|
|
|
|
|
anyhow::bail!("Webhook is not configured. Set a URL and enable it first.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let payload = webhooks::WebhookPayload {
|
|
|
|
|
event: webhooks::WebhookEvent::ContainerCrash,
|
|
|
|
|
title: "Test Notification".to_string(),
|
|
|
|
|
message: "This is a test webhook from your Archipelago node.".to_string(),
|
|
|
|
|
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
|
|
|
node_id: {
|
|
|
|
|
let (data, _) = self.state_manager.get_snapshot().await;
|
|
|
|
|
data.server_info.id
|
|
|
|
|
},
|
|
|
|
|
details: Some(serde_json::json!({"test": true})),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
webhooks::send_webhook(&self.config.data_dir, payload).await;
|
|
|
|
|
|
|
|
|
|
Ok(serde_json::json!({
|
|
|
|
|
"sent": true,
|
|
|
|
|
"url": config.url,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
}
|