pub const BIND_ADDR: &str = "127.0.0.1:5784";
pub(crate) const BIND_ADDR_ENV: &str = "MOADIM_BIND_ADDR";
pub(crate) const API_TOKEN_ENV: &str = "MOADIM_API_TOKEN";
pub(crate) fn api_token() -> Option<String> {
std::env::var(API_TOKEN_ENV)
.ok()
.map(|token| token.trim().to_string())
.filter(|token| !token.is_empty())
}
pub fn api_token_configured() -> bool {
api_token().is_some()
}
pub fn bind_addr() -> String {
std::env::var(BIND_ADDR_ENV).unwrap_or_else(|_| BIND_ADDR.to_string())
}
pub fn bind_addr_is_loopback(addr: &str) -> bool {
addr.parse::<std::net::SocketAddr>()
.is_ok_and(|socket| socket.ip().is_loopback())
}
const ALLOW_REMOTE_ENV: &str = "MOADIM_ALLOW_REMOTE";
pub fn remote_bind_allowed() -> bool {
std::env::var(ALLOW_REMOTE_ENV).as_deref() == Ok("1")
}
#[derive(Debug, PartialEq, Eq)]
pub enum BindDecision {
Loopback,
RemoteAllowed,
RemoteRefused,
}
pub fn classify_bind(addr: &str, allow_remote: bool) -> BindDecision {
if bind_addr_is_loopback(addr) {
BindDecision::Loopback
} else if allow_remote {
BindDecision::RemoteAllowed
} else {
BindDecision::RemoteRefused
}
}
pub fn validated_bind_addr() -> Result<String, String> {
let addr = bind_addr();
let allow_remote = remote_bind_allowed() || api_token_configured();
match classify_bind(&addr, allow_remote) {
BindDecision::Loopback | BindDecision::RemoteAllowed => Ok(addr),
BindDecision::RemoteRefused => Err(format!(
"refusing to bind to {addr}: it is not loopback-only and MOADIM_API_TOKEN is not set. \
Set MOADIM_API_TOKEN to protect REST/MCP with a bearer token, or set \
MOADIM_ALLOW_REMOTE=1 to start without auth if you understand and accept the RCE risk."
)),
}
}