116 lines
4.4 KiB
Rust
Raw Normal View History

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