static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
use webhooksmith::{EventStatus, WebhookEngine};
use webhooksmith::worker::SsrfSafeDnsResolver;
use reqwest::dns::{Resolve, Name};
use serde_json::json;
use std::str::FromStr;
use sqlx::PgPool;
fn engine(pool: PgPool) -> WebhookEngine {
WebhookEngine::builder()
.pool(pool)
.allow_insecure_urls()
.build_sync()
}
#[test]
fn loopback_is_blocked() {
use std::net::IpAddr;
let ip: IpAddr = "127.0.0.1".parse().unwrap();
assert!(ip.is_loopback());
}
#[test]
fn private_ranges_are_blocked() {
use webhooksmith::model::is_private_ip;
use std::net::IpAddr;
let blocked = [
"10.0.0.1",
"10.255.255.255",
"172.16.0.1",
"172.31.255.255",
"192.168.0.1",
"192.168.255.255",
"169.254.169.254", "169.254.0.1", ];
for ip_str in &blocked {
let ip: IpAddr = ip_str.parse().unwrap();
assert!(
is_private_ip(ip),
"{ip_str} must be identified as private"
);
}
}
#[test]
fn public_ips_are_allowed() {
use webhooksmith::model::is_private_ip;
use std::net::IpAddr;
let allowed = [
"8.8.8.8",
"1.1.1.1",
"93.184.216.34", "172.15.255.255", "172.32.0.0", ];
for ip_str in &allowed {
let ip: IpAddr = ip_str.parse().unwrap();
assert!(
!is_private_ip(ip),
"{ip_str} must not be identified as private"
);
}
}
#[tokio::test]
async fn resolver_blocks_localhost_hostname() {
let resolver = SsrfSafeDnsResolver;
let name = Name::from_str("localhost").unwrap();
let result = resolver.resolve(name).await;
assert!(result.is_err(), "resolver must reject 'localhost' (resolves to 127.0.0.1)");
let err_msg = match result {
Err(e) => e.to_string(),
Ok(_) => unreachable!(),
};
assert!(
err_msg.contains("SSRF"),
"error message must mention SSRF protection, got: {err_msg}"
);
}
#[sqlx::test(migrator = "MIGRATOR")]
async fn delivery_to_private_hostname_is_recorded_as_failure(pool: PgPool) {
let engine = engine(pool);
let endpoint = engine
.register("http://localhost/hook", "dns_rebinding_test_secret_ok")
.await
.unwrap();
let event = engine
.send("test.event", json!({}), endpoint.id)
.await
.unwrap();
engine.run_once().await.unwrap();
let updated = engine.event(event.id).await.unwrap().unwrap();
assert_eq!(
updated.status,
EventStatus::Failed,
"delivery to a private hostname must be recorded as a failure"
);
let log = engine.delivery_log(event.id).await.unwrap();
assert_eq!(log.len(), 1, "one delivery attempt must be recorded");
assert!(!log[0].success, "attempt must be marked unsuccessful");
assert!(
log[0].error.is_some(),
"delivery attempt must record an error message"
);
}