Skip to main content

webex_message_handler/
url_validation.rs

1//! URL validation for external API responses.
2
3const ALLOWED_DOMAIN_SUFFIXES: &[&str] = &[".webex.com", ".wbx2.com", ".ciscospark.com"];
4
5/// Validates that a URL is HTTPS and belongs to a recognized Webex domain.
6///
7/// # Arguments
8/// * `raw_url` - The URL string to validate
9/// * `required_scheme` - The required scheme (typically "https" or "wss")
10///
11/// # Returns
12/// * `Ok(())` if valid
13/// * `Err(String)` with validation error message if invalid
14pub fn validate_webex_url(raw_url: &str, required_scheme: &str) -> Result<(), String> {
15    // Extract scheme
16    let scheme_end = raw_url.find("://").ok_or_else(|| "URL missing scheme".to_string())?;
17    let scheme = &raw_url[..scheme_end];
18
19    if scheme != required_scheme {
20        return Err(format!(
21            "URL scheme must be {required_scheme}, got {scheme}"
22        ));
23    }
24
25    // Extract host portion
26    let after_scheme = &raw_url[scheme_end + 3..];
27    let host = after_scheme.split('/').next().unwrap_or("");
28    let host_lower = host.to_lowercase();
29
30    // Remove port if present
31    let host_without_port = host_lower.split(':').next().unwrap_or(&host_lower);
32
33    // Check if host ends with an allowed domain
34    // Allow both *.webex.com format and webex.com format (bare domain)
35    let is_allowed = ALLOWED_DOMAIN_SUFFIXES
36        .iter()
37        .any(|suffix| host_without_port.ends_with(suffix) || host_without_port == &suffix[1..]);
38
39    if !is_allowed {
40        return Err(format!(
41            "URL host {host_without_port} is not a recognized Webex domain"
42        ));
43    }
44
45    Ok(())
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn test_valid_webex_com_https() {
54        assert!(validate_webex_url("https://webex.com/api/v1", "https").is_ok());
55    }
56
57    #[test]
58    fn test_valid_wbx2_com_https() {
59        assert!(validate_webex_url("https://wdm-a.wbx2.com/wdm/api", "https").is_ok());
60    }
61
62    #[test]
63    fn test_valid_ciscospark_com_https() {
64        assert!(validate_webex_url("https://api.ciscospark.com/v1", "https").is_ok());
65    }
66
67    #[test]
68    fn test_valid_wss_scheme() {
69        assert!(validate_webex_url("wss://mercury.webex.com/socket", "wss").is_ok());
70    }
71
72    #[test]
73    fn test_invalid_scheme() {
74        let result = validate_webex_url("http://webex.com/api", "https");
75        assert!(result.is_err());
76        assert!(result.unwrap_err().contains("scheme must be https"));
77    }
78
79    #[test]
80    fn test_invalid_domain() {
81        let result = validate_webex_url("https://evil.com/api", "https");
82        assert!(result.is_err());
83        assert!(result.unwrap_err().contains("not a recognized Webex domain"));
84    }
85
86    #[test]
87    fn test_url_with_port() {
88        assert!(validate_webex_url("https://webex.com:443/api", "https").is_ok());
89    }
90
91    #[test]
92    fn test_missing_scheme() {
93        let result = validate_webex_url("webex.com/api", "https");
94        assert!(result.is_err());
95    }
96}