Skip to main content

worldinterface_http_trigger/
validation.rs

1//! Webhook path validation.
2
3use crate::error::WebhookError;
4
5/// Validate a webhook path.
6///
7/// Rules:
8/// - Must be non-empty
9/// - Must not start or end with '/'
10/// - Must contain only alphanumeric characters, hyphens, underscores, dots, and slashes
11/// - Must not contain '..' (directory traversal)
12/// - Maximum length: 256 characters
13pub fn validate_webhook_path(path: &str) -> Result<(), WebhookError> {
14    if path.is_empty() {
15        return Err(WebhookError::InvalidPath("path must not be empty".into()));
16    }
17    if path.starts_with('/') || path.ends_with('/') {
18        return Err(WebhookError::InvalidPath("path must not start or end with '/'".into()));
19    }
20    if path.contains("..") {
21        return Err(WebhookError::InvalidPath("path must not contain '..'".into()));
22    }
23    if path.len() > 256 {
24        return Err(WebhookError::InvalidPath("path must be 256 characters or fewer".into()));
25    }
26    let valid = path
27        .chars()
28        .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/');
29    if !valid {
30        return Err(WebhookError::InvalidPath(
31            "path contains invalid characters (allowed: alphanumeric, -, _, ., /)".into(),
32        ));
33    }
34    Ok(())
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40
41    #[test]
42    fn valid_simple_path() {
43        assert!(validate_webhook_path("github/push").is_ok());
44    }
45
46    #[test]
47    fn valid_nested_path() {
48        assert!(validate_webhook_path("org/repo/branch").is_ok());
49    }
50
51    #[test]
52    fn valid_path_with_dots() {
53        assert!(validate_webhook_path("api.v1.hook").is_ok());
54    }
55
56    #[test]
57    fn valid_path_with_hyphens() {
58        assert!(validate_webhook_path("my-webhook").is_ok());
59    }
60
61    #[test]
62    fn valid_path_with_underscores() {
63        assert!(validate_webhook_path("my_webhook").is_ok());
64    }
65
66    #[test]
67    fn rejects_empty_path() {
68        let err = validate_webhook_path("").unwrap_err();
69        assert!(matches!(err, WebhookError::InvalidPath(_)));
70    }
71
72    #[test]
73    fn rejects_leading_slash() {
74        let err = validate_webhook_path("/github/push").unwrap_err();
75        assert!(matches!(err, WebhookError::InvalidPath(_)));
76    }
77
78    #[test]
79    fn rejects_trailing_slash() {
80        let err = validate_webhook_path("github/push/").unwrap_err();
81        assert!(matches!(err, WebhookError::InvalidPath(_)));
82    }
83
84    #[test]
85    fn rejects_directory_traversal() {
86        let err = validate_webhook_path("../secret").unwrap_err();
87        assert!(matches!(err, WebhookError::InvalidPath(_)));
88    }
89
90    #[test]
91    fn rejects_special_characters() {
92        let err = validate_webhook_path("hook?q=1").unwrap_err();
93        assert!(matches!(err, WebhookError::InvalidPath(_)));
94    }
95
96    #[test]
97    fn rejects_too_long_path() {
98        let long = "a".repeat(257);
99        let err = validate_webhook_path(&long).unwrap_err();
100        assert!(matches!(err, WebhookError::InvalidPath(_)));
101    }
102
103    #[test]
104    fn accepts_max_length_path() {
105        let max = "a".repeat(256);
106        assert!(validate_webhook_path(&max).is_ok());
107    }
108}