pub fn template_to_regex(template: &str) -> Option<regex::Regex> {
let hostname = if let Some(slash_pos) = template.find('/') {
&template[..slash_pos]
} else {
template
};
let mut regex_pattern = String::from("^");
let mut current_pos = 0;
while current_pos < hostname.len() {
if let Some(start) = hostname[current_pos..].find('{') {
let start_pos = current_pos + start;
if start_pos > current_pos {
regex_pattern.push_str(®ex::escape(&hostname[current_pos..start_pos]));
}
if let Some(end) = hostname[start_pos..].find('}') {
let end_pos = start_pos + end;
regex_pattern.push_str(r"[a-z0-9]([a-z0-9-]*[a-z0-9])?");
current_pos = end_pos + 1;
} else {
regex_pattern.push_str(®ex::escape(&hostname[current_pos..]));
break;
}
} else {
regex_pattern.push_str(®ex::escape(&hostname[current_pos..]));
break;
}
}
regex_pattern.push('$');
regex::Regex::new(®ex_pattern).ok()
}
fn extract_hostname_from_url(url: &str) -> Option<String> {
let without_scheme = if let Some(pos) = url.find("://") {
&url[pos + 3..]
} else {
url
};
let hostname = without_scheme
.split(&[':', '/'][..])
.next()
.unwrap_or("")
.to_string();
if hostname.is_empty() {
None
} else {
Some(hostname)
}
}
pub fn validate_custom_domain(
domain: &str,
production_template: &str,
staging_template: Option<&str>,
rise_public_url: Option<&str>,
) -> Result<(), String> {
if let Some(public_url) = rise_public_url {
if let Some(rise_hostname) = extract_hostname_from_url(public_url) {
if domain == rise_hostname {
return Err(format!(
"Custom domain '{}' conflicts with Rise's public URL hostname",
domain
));
}
}
}
if let Some(regex) = template_to_regex(production_template) {
if regex.is_match(domain) {
return Err(format!(
"Custom domain '{}' conflicts with the project default domain pattern (production template)",
domain
));
}
}
if let Some(staging_template) = staging_template {
if let Some(regex) = template_to_regex(staging_template) {
if regex.is_match(domain) {
return Err(format!(
"Custom domain '{}' conflicts with the staging deployment domain pattern",
domain
));
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_template_to_regex_subdomain() {
let regex = template_to_regex("{project_name}.apps.example.com").unwrap();
assert!(regex.is_match("foo.apps.example.com"));
assert!(regex.is_match("bar.apps.example.com"));
assert!(!regex.is_match("apps.example.com")); assert!(!regex.is_match("foo.bar.apps.example.com")); assert!(!regex.is_match("other.com"));
}
#[test]
fn test_template_to_regex_path_based() {
let regex = template_to_regex("example.com/{project_name}").unwrap();
assert!(regex.is_match("example.com"));
assert!(!regex.is_match("other.com"));
assert!(!regex.is_match("foo.example.com"));
}
#[test]
fn test_template_to_regex_mixed() {
let regex = template_to_regex("{project_name}.example.com/{deployment_group}").unwrap();
assert!(regex.is_match("foo.example.com"));
assert!(regex.is_match("bar.example.com"));
assert!(!regex.is_match("example.com"));
assert!(!regex.is_match("foo.bar.example.com"));
}
#[test]
fn test_template_to_regex_staging() {
let regex =
template_to_regex("{project_name}-{deployment_group}.preview.example.com").unwrap();
assert!(regex.is_match("foo-bar.preview.example.com"));
assert!(regex.is_match("a-b.preview.example.com"));
assert!(!regex.is_match("foobar.preview.example.com"));
assert!(!regex.is_match("foo.preview.example.com"));
}
#[test]
fn test_extract_hostname_from_url() {
assert_eq!(
extract_hostname_from_url("https://example.com"),
Some("example.com".to_string())
);
assert_eq!(
extract_hostname_from_url("http://example.com:8080"),
Some("example.com".to_string())
);
assert_eq!(
extract_hostname_from_url("example.com"),
Some("example.com".to_string())
);
assert_eq!(
extract_hostname_from_url("example.com:8080"),
Some("example.com".to_string())
);
assert_eq!(
extract_hostname_from_url("http://example.com/path"),
Some("example.com".to_string())
);
}
#[test]
fn test_validate_custom_domain_subdomain_conflict() {
let result = validate_custom_domain(
"bar.apps.example.com",
"{project_name}.apps.example.com",
None,
None,
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("conflicts with the project default domain pattern"));
}
#[test]
fn test_validate_custom_domain_subdomain_ok() {
let result = validate_custom_domain(
"mycustomdomain.com",
"{project_name}.apps.example.com",
None,
None,
);
assert!(result.is_ok());
}
#[test]
fn test_validate_custom_domain_path_based_conflict() {
let result =
validate_custom_domain("example.com", "example.com/{project_name}", None, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("conflicts with the project default domain pattern"));
}
#[test]
fn test_validate_custom_domain_path_based_ok() {
let result = validate_custom_domain("other.com", "example.com/{project_name}", None, None);
assert!(result.is_ok());
}
#[test]
fn test_validate_custom_domain_staging_conflict() {
let result = validate_custom_domain(
"foo-bar.preview.example.com",
"{project_name}.apps.example.com",
Some("{project_name}-{deployment_group}.preview.example.com"),
None,
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("staging deployment domain pattern"));
}
#[test]
fn test_validate_custom_domain_multiple_levels() {
let result = validate_custom_domain(
"foo.bar.apps.example.com",
"{project_name}.apps.example.com",
None,
None,
);
assert!(result.is_ok()); }
#[test]
fn test_validate_custom_domain_rise_public_url_conflict() {
let result = validate_custom_domain(
"rise.example.com",
"{project_name}.apps.example.com",
None,
Some("https://rise.example.com"),
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("conflicts with Rise's public URL"));
}
#[test]
fn test_validate_custom_domain_rise_public_url_ok() {
let result = validate_custom_domain(
"mycustomdomain.com",
"{project_name}.apps.example.com",
None,
Some("https://rise.example.com"),
);
assert!(result.is_ok());
}
#[test]
fn test_validate_custom_domain_mixed_template() {
let result = validate_custom_domain(
"foo.example.com",
"{project_name}.example.com/{deployment_group}",
None,
None,
);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("conflicts with the project default domain pattern"));
}
}