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 { 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, ) -> Result { 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()) { // 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"); } } 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::>(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 { 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, })) } }