//! Listener-aware host-owned local responses for domains that intentionally do //! not proxy to an upstream. These hosts reserve their frontend names exactly //! like vhosts/redirects and can serve a built-in placeholder, empty status, or //! administrator-provided bounded HTML. use anyhow::{ensure, Result}; use serde::{Deserialize, Serialize}; fn yes() -> bool { true } fn default_status() -> u16 { 404 } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Host { pub id: String, #[serde(default)] pub name: String, #[serde(default = "yes")] pub enabled: bool, #[serde(default = "yes")] pub http2_enabled: bool, #[serde(default = "yes")] pub http3_enabled: bool, pub frontend: crate::vhost::Frontend, #[serde(default, skip_serializing_if = "Option::is_none")] pub certificate_id: Option, #[serde(default = "default_page")] pub page: crate::pages::Page, } fn default_page() -> crate::pages::Page { crate::pages::Page { kind: crate::pages::Kind::Welcome, status: Some(default_status()), html: String::new(), } } impl Host { pub fn validate(&mut self) -> Result<()> { ensure!(crate::config::valid_id(&self.id), "invalid 404 host id"); if self.name.trim().is_empty() { self.name = self.id.clone(); } self.name = self.name.trim().to_owned(); ensure!( !self.name.is_empty() && self.name.len() <= 96 && !self.name.chars().any(char::is_control), "404 host name must be 1..96 bytes without controls" ); ensure!( !self.frontend.hosts.is_empty(), "404 host requires at least one hostname" ); let mut hosts = std::collections::HashSet::new(); for host in &mut self.frontend.hosts { *host = crate::config::normalize_host_pattern(host)?; ensure!(hosts.insert(host.clone()), "duplicate 404 hostname"); } ensure!( !self.frontend.listener_ids.is_empty() && self.frontend.listener_ids.len() <= 16, "404 host requires 1..16 named listeners" ); if self.page.status.is_none() { self.page.status = Some(default_status()); } self.page.validate_local_response()?; let status = self.page.status.unwrap_or(default_status()); if matches!(self.page.kind, crate::pages::Kind::Builtin) { ensure!( crate::pages::BUILTIN_ERROR_CODES.contains(&status), "application-style 404 host status must use a built-in error template" ); } else { ensure!( crate::pages::ERROR_CODES.contains(&status), "404 host status must use a supported error code" ); } Ok(()) } } #[derive(Debug, Clone)] pub struct CompiledHost { pub id: String, pub bindings: Vec, pub http2_enabled: bool, pub http3_enabled: bool, pub page: crate::pages::Page, } pub fn compile(host: &Host, listeners: &crate::settings::Listeners) -> Result { Ok(CompiledHost { id: host.id.clone(), bindings: host.frontend.resolved(listeners)?, http2_enabled: host.http2_enabled, http3_enabled: host.http3_enabled, page: host.page.clone(), }) } #[cfg(test)] include!("not_found/tests.rs");