diff --git a/server/svix-server/src/cfg.rs b/server/svix-server/src/cfg.rs index f76c5be32..ad5a0d6e7 100644 --- a/server/svix-server/src/cfg.rs +++ b/server/svix-server/src/cfg.rs @@ -11,11 +11,13 @@ use figment::{ use ipnet::IpNet; use serde::{Deserialize, Deserializer}; use tracing::Level; +use url::Url; use validator::{Validate, ValidationError}; use crate::{ core::{cryptography::Encryption, security::JwtSigningConfig}, error::Result, + v1::utils::validation_error, }; fn deserialize_main_secret<'de, D>(deserializer: D) -> Result @@ -74,6 +76,36 @@ fn default_redis_pending_duration_secs() -> u64 { 45 } +fn validate_operational_webhook_url(url: &str) -> Result<(), ValidationError> { + match Url::parse(url) { + Ok(url) => { + // Verify scheme is http or https + if url.scheme() != "http" && url.scheme() != "https" { + return Err(validation_error( + Some("operational_webhook_address"), + Some("URL scheme must be http or https"), + )); + } + + // Verify there's a host + if url.host().is_none() { + return Err(validation_error( + Some("operational_webhook_address"), + Some("URL must include a valid host"), + )); + } + } + Err(_) => { + return Err(validation_error( + Some("operational_webhook_address"), + Some("Invalid URL format"), + )); + } + } + + Ok(()) +} + #[derive(Clone, Debug, Deserialize, Validate)] #[validate( schema(function = "validate_config_complete"), @@ -85,6 +117,7 @@ pub struct ConfigurationInner { /// The address to send operational webhooks to. When None, operational webhooks will not be /// sent. When Some, the API server with the given URL will be used to send operational webhooks. + #[validate(custom = "validate_operational_webhook_url")] pub operational_webhook_address: Option, /// The main secret used by Svix. Used for client-side encryption of sensitive data, etc. diff --git a/server/svix-server/src/core/operational_webhooks.rs b/server/svix-server/src/core/operational_webhooks.rs index 58ef2c5c3..9f4c33dcb 100644 --- a/server/svix-server/src/core/operational_webhooks.rs +++ b/server/svix-server/src/core/operational_webhooks.rs @@ -115,7 +115,15 @@ pub struct OperationalWebhookSenderInner { } impl OperationalWebhookSenderInner { - pub fn new(keys: Arc, url: Option) -> Arc { + pub fn new(keys: Arc, mut url: Option) -> Arc { + // Sanitize the URL if present + if let Some(url) = &mut url { + // Remove trailing slashes + while url.ends_with('/') { + url.pop(); + } + } + Arc::new(Self { signing_config: keys, url, @@ -177,7 +185,7 @@ impl OperationalWebhookSenderInner { .. })) => { tracing::warn!( - "Operational webhooks are enabled but no listener set for {}", + "Operational webhooks are enabled, but no listener found for organization {}", recipient_org_id, ); }