use url::Url;
pub struct URIValidator;
impl URIValidator {
const DOMAIN_PROTOCOLS: &[&str] = &["http:", "https:", "ws:", "wss:"];
const AZURE_SDK_PROTOCOLS: &[&str] = &["http:", "https:"];
pub fn in_domain(url: &str, trusted_domains: &[&str]) -> bool {
let hostname = match Self::get_valid_hostname(url, Self::DOMAIN_PROTOCOLS) {
Some(h) => h,
None => return false,
};
if trusted_domains.is_empty() {
return false;
}
for domain in trusted_domains {
let ascii_domain = match Self::domain_to_ascii(domain) {
Some(d) => d,
None => continue,
};
if Self::hostname_in_single_domain(&hostname, &ascii_domain) {
return true;
}
}
false
}
pub fn in_azure_key_vault_domain(url: &str) -> bool {
let hostname = match Self::get_valid_hostname(url, Self::AZURE_SDK_PROTOCOLS) {
Some(h) => h,
None => return false,
};
if hostname.contains("--") {
return false;
}
for domain in crate::domains::AZURE_KEY_VAULT_DOMAINS {
if Self::hostname_in_single_domain(&hostname, domain) {
return true;
}
}
false
}
pub fn in_azure_storage_domain(url: &str) -> bool {
let hostname = match Self::get_valid_hostname(url, Self::AZURE_SDK_PROTOCOLS) {
Some(h) => h,
None => return false,
};
if hostname.contains("--") {
return false;
}
for domain in crate::domains::AZURE_STORAGE_DOMAINS {
if Self::hostname_in_single_domain(&hostname, domain) {
return true;
}
}
false
}
fn get_valid_hostname(url_str: &str, allowed_protocols: &[&str]) -> Option<String> {
let url = Url::parse(url_str).ok()?;
let hostname = url.host_str()?.to_string();
if hostname.is_empty() {
return None;
}
let protocol = format!("{}:", url.scheme());
if !allowed_protocols.contains(&protocol.as_str()) {
return None;
}
Some(hostname)
}
fn domain_to_ascii(domain: &str) -> Option<String> {
let temp_url = format!("https://{}/", domain);
let url = Url::parse(&temp_url).ok()?;
url.host_str().map(|s| s.to_string())
}
fn hostname_in_single_domain(hostname: &str, domain: &str) -> bool {
let dotted = format!(".{}", hostname);
if dotted.ends_with(domain) {
if hostname.len() == domain.len() {
return true;
}
if domain.starts_with('.') {
return true;
}
if hostname.len() > domain.len()
&& hostname.as_bytes()[hostname.len() - domain.len() - 1] == b'.'
{
return true;
}
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_domain_exact_match() {
assert!(URIValidator::in_domain(
"https://trusted.com",
&["trusted.com"]
));
}
#[test]
fn in_domain_subdomain_match() {
assert!(URIValidator::in_domain(
"https://api.trusted.com",
&["trusted.com"]
));
assert!(URIValidator::in_domain(
"https://sub.api.trusted.com",
&["trusted.com"]
));
}
#[test]
fn in_domain_no_false_positive() {
assert!(!URIValidator::in_domain(
"https://nottrusted.com",
&["trusted.com"]
));
assert!(!URIValidator::in_domain(
"https://trusted.com.evil.com",
&["trusted.com"]
));
}
#[test]
fn in_domain_dotted_domain() {
assert!(URIValidator::in_domain(
"https://api.trusted.com",
&[".trusted.com"]
));
assert!(URIValidator::in_domain(
"https://trusted.com",
&[".trusted.com"]
));
}
#[test]
fn in_domain_multiple_domains() {
assert!(URIValidator::in_domain(
"https://api.first.com",
&["first.com", "second.com"]
));
assert!(URIValidator::in_domain(
"https://api.second.com",
&["first.com", "second.com"]
));
}
#[test]
fn in_domain_unsupported_protocol() {
assert!(!URIValidator::in_domain(
"ftp://trusted.com",
&["trusted.com"]
));
assert!(!URIValidator::in_domain(
"file:///trusted.com",
&["trusted.com"]
));
}
#[test]
fn in_domain_supported_protocols() {
assert!(URIValidator::in_domain(
"http://trusted.com",
&["trusted.com"]
));
assert!(URIValidator::in_domain(
"https://trusted.com",
&["trusted.com"]
));
assert!(URIValidator::in_domain(
"ws://trusted.com",
&["trusted.com"]
));
assert!(URIValidator::in_domain(
"wss://trusted.com",
&["trusted.com"]
));
}
#[test]
fn in_domain_invalid_url() {
assert!(!URIValidator::in_domain("not-a-url", &["trusted.com"]));
}
#[test]
fn in_domain_empty_hostname() {
assert!(!URIValidator::in_domain("https://", &["trusted.com"]));
}
#[test]
fn in_domain_empty_trusted_domains() {
assert!(!URIValidator::in_domain("https://trusted.com", &[]));
}
#[test]
fn in_domain_skips_malformed_domains() {
assert!(URIValidator::in_domain(
"https://valid.com",
&["valid.com", "not a domain!"]
));
assert!(URIValidator::in_domain(
"https://valid.com",
&["not a domain!", "valid.com"]
));
assert!(!URIValidator::in_domain(
"https://valid.com",
&["not a domain!", "also bad"]
));
}
#[test]
fn in_domain_invalid_trusted_domain() {
assert!(!URIValidator::in_domain(
"https://trusted.com",
&["not a domain"]
));
}
#[test]
fn in_domain_punycode() {
assert!(URIValidator::in_domain(
"https://münchen.example",
&["xn--mnchen-3ya.example"]
));
}
#[test]
fn in_azure_key_vault_domain_match() {
assert!(URIValidator::in_azure_key_vault_domain(
"https://myvault.vault.azure.net"
));
}
#[test]
fn in_azure_key_vault_domain_subdomain() {
assert!(URIValidator::in_azure_key_vault_domain(
"https://sub.myvault.vault.azure.net"
));
}
#[test]
fn in_azure_key_vault_domain_rejects_double_dash() {
assert!(!URIValidator::in_azure_key_vault_domain(
"https://my--vault.vault.azure.net"
));
}
#[test]
fn in_azure_key_vault_domain_rejects_unsupported_protocol() {
assert!(!URIValidator::in_azure_key_vault_domain(
"ws://myvault.vault.azure.net"
));
}
#[test]
fn in_azure_key_vault_domain_no_match() {
assert!(!URIValidator::in_azure_key_vault_domain("https://evil.com"));
}
#[test]
fn in_azure_storage_domain_match() {
assert!(URIValidator::in_azure_storage_domain(
"https://mystorage.blob.core.windows.net"
));
}
#[test]
fn in_azure_storage_domain_rejects_double_dash() {
assert!(!URIValidator::in_azure_storage_domain(
"https://my--storage.blob.core.windows.net"
));
}
#[test]
fn in_azure_storage_domain_no_match() {
assert!(!URIValidator::in_azure_storage_domain("https://evil.com"));
}
#[test]
fn hostname_in_single_domain_exact() {
assert!(URIValidator::hostname_in_single_domain(
"trusted.com",
"trusted.com"
));
}
#[test]
fn hostname_in_single_domain_subdomain() {
assert!(URIValidator::hostname_in_single_domain(
"api.trusted.com",
"trusted.com"
));
}
#[test]
fn hostname_in_single_domain_no_false_match() {
assert!(!URIValidator::hostname_in_single_domain(
"nottrusted.com",
"trusted.com"
));
assert!(!URIValidator::hostname_in_single_domain(
"trusted.com.evil",
"trusted.com"
));
}
#[test]
fn hostname_in_single_domain_dotted() {
assert!(URIValidator::hostname_in_single_domain(
"api.trusted.com",
".trusted.com"
));
assert!(URIValidator::hostname_in_single_domain(
"trusted.com",
".trusted.com"
));
}
}