use std::env;
use std::fs;
use std::net::IpAddr;
use std::path::Path;
const AUTH_TOKEN_FILENAME: &str = "auth-token";
pub const AUTHORIZATION_HEADER: &str = "Authorization";
pub const BEARER_PREFIX: &str = "Bearer ";
use subtle::ConstantTimeEq;
#[cfg(unix)]
fn check_token_file_permissions(path: &Path) {
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
let mode = metadata.permissions().mode();
if mode & 0o077 != 0 {
tracing::warn!(
"Token file {} has insecure permissions ({:04o}). Should be 0600.",
path.display(),
mode & 0o777
);
}
}
}
#[cfg(not(unix))]
fn check_token_file_permissions(path: &Path) {
tracing::debug!(
"Skipping permission check for token file {} (not Unix)",
path.display()
);
}
pub fn discover_token(detrix_home: Option<&Path>) -> Option<String> {
if let Ok(token) = env::var("DETRIX_TOKEN") {
if !token.is_empty() {
return Some(token);
}
}
if let Some(home) = dirs::home_dir() {
let token_path = home.join("detrix").join(AUTH_TOKEN_FILENAME);
check_token_file_permissions(&token_path);
if let Ok(token) = fs::read_to_string(&token_path) {
let token = token.trim().to_string();
if !token.is_empty() {
return Some(token);
}
}
}
if let Some(home) = detrix_home {
let token_path = home.join(AUTH_TOKEN_FILENAME);
check_token_file_permissions(&token_path);
if let Ok(token) = fs::read_to_string(&token_path) {
let token = token.trim().to_string();
if !token.is_empty() {
return Some(token);
}
}
}
None
}
pub fn is_localhost(addr: &str) -> bool {
if addr.starts_with('[') {
if let Some(bracket_end) = addr.find(']') {
let host = &addr[1..bracket_end];
return check_localhost_host(host);
}
}
if let Some(idx) = addr.rfind(':') {
let potential_host = &addr[..idx];
if !potential_host.contains(':') {
return check_localhost_host(potential_host);
}
}
check_localhost_host(addr)
}
fn check_localhost_host(host: &str) -> bool {
if host.eq_ignore_ascii_case("localhost") || host == "127.0.0.1" || host == "::1" {
return true;
}
if let Ok(ip) = host.parse::<IpAddr>() {
return ip.is_loopback();
}
false
}
pub fn is_authorized(
remote_addr: &str,
auth_header: Option<&str>,
valid_token: Option<&str>,
) -> bool {
if is_localhost(remote_addr) {
return true;
}
let valid_token = match valid_token {
Some(t) if !t.is_empty() => t,
_ => return false,
};
let auth_header = match auth_header {
Some(h) => h,
None => return false,
};
if !auth_header.starts_with(BEARER_PREFIX) {
return false;
}
let token = &auth_header[BEARER_PREFIX.len()..];
token.as_bytes().ct_eq(valid_token.as_bytes()).into()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
static ENV_MUTEX: Mutex<()> = Mutex::new(());
#[test]
fn test_is_localhost_ipv4() {
assert!(is_localhost("127.0.0.1"));
assert!(is_localhost("127.0.0.1:8080"));
assert!(!is_localhost("192.168.1.1"));
assert!(!is_localhost("192.168.1.1:8080"));
}
#[test]
fn test_is_localhost_ipv6() {
assert!(is_localhost("::1"));
assert!(is_localhost("[::1]:8080"));
assert!(!is_localhost("::2"));
}
#[test]
fn test_is_localhost_name() {
assert!(is_localhost("localhost"));
assert!(is_localhost("localhost:8080"));
assert!(is_localhost("LOCALHOST"));
assert!(!is_localhost("example.com"));
}
#[test]
fn test_check_localhost_case_insensitive() {
assert!(is_localhost("Localhost"));
assert!(is_localhost("LOCALHOST"));
assert!(is_localhost("LocalHost:9090"));
}
#[test]
fn test_is_authorized_localhost() {
assert!(is_authorized("127.0.0.1:12345", None, None));
assert!(is_authorized("127.0.0.1:12345", None, Some("secret")));
assert!(is_authorized("::1", None, None));
assert!(is_authorized("localhost:8080", None, None));
}
#[test]
fn test_is_authorized_remote_no_token() {
assert!(!is_authorized("192.168.1.1:12345", None, None));
assert!(!is_authorized("192.168.1.1:12345", None, Some("")));
}
#[test]
fn test_is_authorized_remote_with_token() {
let token = "secret-token";
assert!(is_authorized(
"192.168.1.1:12345",
Some("Bearer secret-token"),
Some(token)
));
assert!(!is_authorized(
"192.168.1.1:12345",
Some("Bearer wrong-token"),
Some(token)
));
assert!(!is_authorized(
"192.168.1.1:12345",
Some("secret-token"),
Some(token)
));
assert!(!is_authorized("192.168.1.1:12345", None, Some(token)));
}
#[test]
fn test_discover_token_from_env() {
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let orig = std::env::var("DETRIX_TOKEN").ok();
std::env::set_var("DETRIX_TOKEN", "test-token-123");
let token = discover_token(None);
match orig {
Some(v) => std::env::set_var("DETRIX_TOKEN", v),
None => std::env::remove_var("DETRIX_TOKEN"),
}
assert_eq!(token, Some("test-token-123".to_string()));
}
#[test]
fn test_discover_token_from_file() {
use std::io::Write;
use tempfile::TempDir;
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let orig = std::env::var("DETRIX_TOKEN").ok();
std::env::remove_var("DETRIX_TOKEN");
let temp_dir = TempDir::new().unwrap();
let token_path = temp_dir.path().join(AUTH_TOKEN_FILENAME);
let mut file = std::fs::File::create(&token_path).unwrap();
writeln!(file, "file-token-456").unwrap();
let token = discover_token(Some(temp_dir.path()));
if let Some(v) = orig {
std::env::set_var("DETRIX_TOKEN", v);
}
assert!(token.is_some());
}
#[test]
fn test_discover_token_trims_whitespace() {
use std::io::Write;
use tempfile::TempDir;
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let orig = std::env::var("DETRIX_TOKEN").ok();
std::env::remove_var("DETRIX_TOKEN");
let temp_dir = TempDir::new().unwrap();
let token_path = temp_dir.path().join(AUTH_TOKEN_FILENAME);
let mut file = std::fs::File::create(&token_path).unwrap();
writeln!(file, " token-with-spaces \n").unwrap();
let token = discover_token(Some(temp_dir.path()));
if let Some(v) = orig {
std::env::set_var("DETRIX_TOKEN", v);
}
assert!(token.is_some());
let t = token.unwrap();
assert!(!t.starts_with(' '));
assert!(!t.ends_with(' '));
assert!(!t.ends_with('\n'));
}
#[test]
fn test_discover_token_env_takes_priority() {
use std::io::Write;
use tempfile::TempDir;
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let orig = std::env::var("DETRIX_TOKEN").ok();
std::env::set_var("DETRIX_TOKEN", "env-token");
let temp_dir = TempDir::new().unwrap();
let token_path = temp_dir.path().join(AUTH_TOKEN_FILENAME);
let mut file = std::fs::File::create(&token_path).unwrap();
writeln!(file, "file-token").unwrap();
let token = discover_token(Some(temp_dir.path()));
match orig {
Some(v) => std::env::set_var("DETRIX_TOKEN", v),
None => std::env::remove_var("DETRIX_TOKEN"),
}
assert_eq!(token, Some("env-token".to_string()));
}
#[test]
fn test_discover_token_empty_env_ignored() {
let _guard = ENV_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
let orig = std::env::var("DETRIX_TOKEN").ok();
std::env::set_var("DETRIX_TOKEN", "");
let token = discover_token(None);
match orig {
Some(v) => std::env::set_var("DETRIX_TOKEN", v),
None => std::env::remove_var("DETRIX_TOKEN"),
}
assert!(token.is_none() || token.is_some());
}
#[test]
fn test_is_localhost_0000_not_localhost() {
assert!(!is_localhost("0.0.0.0:8080"));
assert!(!is_localhost("0.0.0.0"));
}
}