use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ListenEndpoints {
pub h2: SocketAddr,
pub h3: SocketAddr,
pub h2_enabled: bool,
pub h3_enabled: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StaticSurface {
pub root: PathBuf,
pub prefix: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProxySurface {
pub upstream: String,
pub prefix: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WebSocketSurface {
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrpcSurface {
pub listen: SocketAddr,
}
impl GrpcSurface {
pub fn from_listen(raw: &str) -> Result<Self, String> {
raw.parse::<SocketAddr>()
.map(|listen| Self { listen })
.map_err(|e| format!("invalid grpc listen address: {e}"))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SurfaceConfig {
pub listen: ListenEndpoints,
pub static_files: Option<StaticSurface>,
pub proxy: Option<ProxySurface>,
pub websocket: Option<WebSocketSurface>,
pub grpc: Option<GrpcSurface>,
pub tls_reload: bool,
pub probe_h3: bool,
}
impl SurfaceConfig {
#[must_use]
pub fn from_env() -> Self {
let bind = bind_addr_from_env();
let h2_port = port_from_env("NAUTALID_H2_PORT").unwrap_or_else(default_port);
let h3_port = port_from_env("NAUTALID_H3_PORT").unwrap_or_else(default_port);
Self {
listen: ListenEndpoints {
h2: SocketAddr::new(bind, h2_port),
h3: SocketAddr::new(bind, h3_port),
h2_enabled: !env_disabled("NAUTALID_H2"),
h3_enabled: !env_disabled("NAUTALID_H3"),
},
static_files: static_from_env(),
proxy: proxy_from_env(),
websocket: websocket_from_env(),
grpc: grpc_from_env(),
tls_reload: env_enabled("NAUTALID_TLS_RELOAD"),
probe_h3: env_enabled("NAUTALID_PROBE_H3"),
}
}
}
#[must_use]
pub fn bind_addr_from_env() -> IpAddr {
std::env::var("NAUTALID_BIND_ADDR")
.ok()
.and_then(|raw| raw.trim().parse().ok())
.unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
}
#[must_use]
pub fn default_port() -> u16 {
port_from_env("PORT").unwrap_or(8080)
}
fn port_from_env(name: &str) -> Option<u16> {
std::env::var(name)
.ok()
.and_then(|raw| raw.trim().parse().ok())
}
fn static_from_env() -> Option<StaticSurface> {
if !env_enabled("NAUTALID_STATIC") {
return None;
}
let root = std::env::var("NAUTALID_STATIC_ROOT")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.map(PathBuf::from)?;
let prefix = std::env::var("NAUTALID_STATIC_PREFIX")
.ok()
.map(|s| s.trim().to_string())
.filter(|p| p.starts_with('/'))
.unwrap_or_else(|| "/static".to_string());
Some(StaticSurface { root, prefix })
}
fn proxy_from_env() -> Option<ProxySurface> {
if !env_enabled("NAUTALID_PROXY") {
return None;
}
let upstream = std::env::var("NAUTALID_PROXY_UPSTREAM")
.ok()
.map(|s| s.trim().trim_end_matches('/').to_string())
.filter(|s| !s.is_empty())?;
let prefix = std::env::var("NAUTALID_PROXY_PREFIX")
.ok()
.map(|s| s.trim().to_string())
.filter(|p| p.starts_with('/'))
.unwrap_or_else(|| "/proxy".to_string());
Some(ProxySurface { upstream, prefix })
}
fn websocket_from_env() -> Option<WebSocketSurface> {
if !env_enabled("NAUTALID_WEBSOCKET") {
return None;
}
let path = std::env::var("NAUTALID_WEBSOCKET_PATH")
.ok()
.map(|s| s.trim().to_string())
.filter(|p| p.starts_with('/'))
.unwrap_or_else(|| "/ws".to_string());
Some(WebSocketSurface { path })
}
fn grpc_from_env() -> Option<GrpcSurface> {
if !env_enabled("NAUTALID_GRPC") {
return None;
}
let listen = std::env::var("NAUTALID_GRPC_LISTEN")
.ok()
.filter(|s| !s.trim().is_empty())?;
GrpcSurface::from_listen(listen.trim()).ok()
}
#[must_use]
pub fn env_enabled(name: &str) -> bool {
matches!(
std::env::var(name).ok().as_deref(),
Some("1") | Some("true") | Some("yes")
)
}
#[must_use]
pub fn env_disabled(name: &str) -> bool {
matches!(
std::env::var(name).ok().as_deref(),
Some("0") | Some("false") | Some("no")
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn surfaces_default_off() {
unsafe {
std::env::remove_var("NAUTALID_STATIC");
std::env::remove_var("NAUTALID_PROXY");
std::env::remove_var("NAUTALID_WEBSOCKET");
std::env::remove_var("NAUTALID_GRPC");
std::env::remove_var("NAUTALID_SURFACES_KDL_PATH");
}
let cfg = SurfaceConfig::from_env();
assert!(cfg.static_files.is_none());
assert!(cfg.proxy.is_none());
assert!(cfg.websocket.is_none());
assert!(cfg.grpc.is_none());
}
#[test]
fn static_enabled_requires_root() {
unsafe {
std::env::set_var("NAUTALID_STATIC", "1");
std::env::set_var("NAUTALID_STATIC_ROOT", "/var/www");
std::env::set_var("NAUTALID_STATIC_PREFIX", "/assets");
}
let cfg = SurfaceConfig::from_env();
let surface = cfg.static_files.expect("static");
assert_eq!(surface.root, PathBuf::from("/var/www"));
assert_eq!(surface.prefix, "/assets");
unsafe {
std::env::remove_var("NAUTALID_STATIC");
std::env::remove_var("NAUTALID_STATIC_ROOT");
std::env::remove_var("NAUTALID_STATIC_PREFIX");
}
}
}