use crate::{MiddlewareMechanism, OrdinaryConfig};
use anyhow::bail;
use hashbrown::HashSet;
use regex::Regex;
use url::Url;
pub static DOMAIN_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| {
Regex::new(r"^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$")
.expect("failed to create regex")
});
#[allow(clippy::too_many_lines)]
pub fn validate(config: &OrdinaryConfig) -> anyhow::Result<()> {
let port = config.port.unwrap_or(433);
let redirect_port = config.redirect_port.unwrap_or(80);
if port == redirect_port {
bail!("port and redirect_port cannot be same value {port}");
}
if !DOMAIN_REGEX.is_match(&config.domain) {
bail!("'domain' field is not a valid domain name");
}
if let Some(cnames) = &config.cnames {
if cnames.contains(&config.domain) {
bail!("CNAMEs cannot include the primary domain");
}
for cname in cnames {
if !DOMAIN_REGEX.is_match(cname) {
bail!("cname '{cname}' is not valid domain name");
}
}
}
if let Some(http_config) = &config.http
&& let Some(redirects) = &http_config.redirects
{
if let Some(host_redirects) = &redirects.host {
let mut from_set = HashSet::new();
let mut to_set = HashSet::new();
let mut host_set = HashSet::new();
host_set.insert(config.domain.clone());
if let Some(cnames) = &config.cnames {
for cname in cnames {
host_set.insert(cname.clone());
}
}
for host_redirect in host_redirects {
if !DOMAIN_REGEX.is_match(&host_redirect.from) {
bail!(
"host redirect from '{}' is not valid domain name",
host_redirect.from
);
}
if !DOMAIN_REGEX.is_match(&host_redirect.to) {
bail!(
"host redirect to '{}' is not valid domain name",
host_redirect.to
);
}
if !host_set.contains(&host_redirect.from) {
bail!(
"'{}' for from is not in 'cnames' or the 'domain' field",
host_redirect.from
);
}
if !host_set.contains(&host_redirect.to) {
bail!(
"'{}' for to is not in 'cnames' or the 'domain' field",
host_redirect.from
);
}
if from_set.contains(&host_redirect.from) {
bail!(
"cannot have multiple 'from' host redirect rules for a single host {}",
host_redirect.from
);
}
from_set.insert(host_redirect.from.clone());
if to_set.contains(&host_redirect.from) {
bail!(
"cannot have chained to/from redirects '{}'",
host_redirect.from
);
}
if from_set.contains(&host_redirect.to) {
bail!(
"cannot have chained to/from redirects '{}'",
host_redirect.to
);
}
to_set.insert(host_redirect.to.clone());
}
}
if let Some(route_redirects) = &redirects.route {
for route_redirect in route_redirects {
if !route_redirect.condition.starts_with('/') {
bail!(
"route redirect condition {} must start with a leading forward slash",
route_redirect.condition
);
}
}
}
}
let mut middlewares_set = HashSet::new();
if let Some(http_config) = &config.http
&& let Some(middlewares) = &http_config.middlewares
{
for middleware in middlewares {
middlewares_set.insert(middleware.name.clone());
match &middleware.mechanism {
MiddlewareMechanism::Request { endpoint } => {
Url::parse(endpoint)?;
}
}
}
}
if let Some(http_config) = &config.http
&& let Some(proxies) = &http_config.proxies
{
for proxy in proxies {
if proxy.path.is_none() && proxy.domain.is_none() && proxy.port.is_none() {
bail!(
"neither the proxy `path` nor the proxy `domain` nor the proxy `port` is set for target '{}'",
proxy.target
);
}
if let Some(proxy_port) = &proxy.port {
if proxy_port == &port {
bail!(
"proxy port {proxy_port} for target {} cannot be the same as primary port",
proxy.target
)
}
if proxy_port == &redirect_port {
bail!(
"proxy port {proxy_port} for target {} cannot be the same as redirect_port",
proxy.target
)
}
}
if proxy.path.is_some() && proxy.domain.is_some() {
tracing::warn!(
"`path` {} and `domain` {} should not be set at the same time, only one or the other should be picked; if both are set, only the `path` will be evaluated",
proxy.path.as_ref().unwrap_or(&String::new()),
proxy.domain.as_ref().unwrap_or(&String::new()),
);
}
if let Some(custom_domain) = &proxy.domain {
if !DOMAIN_REGEX.is_match(custom_domain) {
bail!("proxy domain '{custom_domain}' is not valid domain name");
}
if custom_domain == &config.domain {
bail!(
"proxy domain {custom_domain} for target {} is the same as the primary app domain.",
proxy.target
);
}
if let Some(cnames) = &config.cnames
&& cnames.contains(custom_domain)
{
bail!(
"proxy domain {custom_domain} for target {} contained in CNAMEs {cnames:?}",
proxy.target
);
}
let port_str = format!(":{port}");
let redirect_port_str = format!(":{redirect_port}");
if (custom_domain.starts_with("localhost")
|| custom_domain.starts_with("127.0.0.1"))
&& (custom_domain.contains(&port_str)
|| custom_domain.contains(&redirect_port_str))
{
bail!(
"loopback domain {custom_domain} port for target {} collide with primary {port_str} or redirect {redirect_port_str} ports",
proxy.target
);
}
}
if let Some(path) = &proxy.path
&& !path.ends_with("/{*path}")
{
bail!(
"path for target '{}' must end in `/{{*path}}`",
proxy.target
);
}
Url::parse(&proxy.target)?;
if let Some(middlewares) = &proxy.middlewares {
for middleware in middlewares {
if !middlewares_set.contains(middleware) {
bail!(
"middleware '{middleware}' does not exist (for proxy {})",
proxy.target
);
}
}
}
}
}
if let Some(assets) = &config.assets {
if !assets.base_route.starts_with('/') {
bail!("assets base route must start with leading forward slash");
}
if assets.append_index_html == Some(true) && assets.append_html_ext == Some(true) {
tracing::warn!(
"'assets.append_index_html' and 'assets.append_index_ext' will conflict at runtime with 'assets.append_index_html' taking precedence. only one or the other should be selected."
);
}
if let Some(middlewares) = &assets.middlewares {
for middleware in middlewares {
if !middlewares_set.contains(middleware) {
bail!("middleware '{middleware}' does not exist (for assets)");
}
}
}
}
if let Some(database_config) = &config.database {
for model_config in &database_config.models {
for field in &model_config.fields {
if field.idx == 0 {
bail!(
"model {} {} field {}: database model fields cannot use 0th idx as it is reserved for its UUID",
model_config.name,
model_config.idx,
field.name
);
}
}
}
}
if let Some(function_configs) = &config.functions {
for function_config in function_configs {
let Some(function_name) = &function_config.name else {
bail!("function must have a name");
};
if function_config.ffi.is_none() {
bail!("function '{function_name}' must have an 'ffi'");
}
if function_config.input.is_none() {
bail!("function '{function_name}' must have an 'input'");
}
if function_config.output.is_none() {
bail!("function '{function_name}' must have an 'output'");
}
}
}
tracing::info!("valid config");
Ok(())
}