use std::os::unix::fs::MetadataExt;
use std::path::{Path, PathBuf};
use crate::AppResult;
pub fn runtime_dir() -> AppResult<PathBuf> {
match std::env::var("XDG_RUNTIME_DIR") {
Ok(dir) if !dir.is_empty() => Ok(PathBuf::from(dir)),
_ => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"XDG_RUNTIME_DIR environment variable is not set or is empty. \
This is required for secure operation.",
)
.into()),
}
}
pub fn validate_runtime_dir() -> AppResult<PathBuf> {
let dir = runtime_dir()?;
let euid = unsafe { libc::geteuid() };
validate_dir(&dir, euid)?;
Ok(dir)
}
fn validate_dir(dir: &Path, euid: u32) -> std::io::Result<()> {
let md = std::fs::metadata(dir)?; if !md.is_dir() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotADirectory,
format!("XDG_RUNTIME_DIR {} is not a directory", dir.display()),
));
}
if md.uid() != euid {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"XDG_RUNTIME_DIR {} is owned by uid {}, expected {euid}; \
do not run the daemon as root",
dir.display(),
md.uid()
),
));
}
if md.mode() & 0o077 != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!(
"XDG_RUNTIME_DIR {} is accessible by group/other (mode {:o}); refusing",
dir.display(),
md.mode() & 0o777
),
));
}
Ok(())
}
pub fn socket_path() -> AppResult<PathBuf> {
Ok(socket_path_in(&runtime_dir()?))
}
fn socket_path_in(runtime: &Path) -> PathBuf {
runtime.join("eqtui").join("eqtui.sock")
}
pub fn lock_path() -> AppResult<PathBuf> {
Ok(lock_path_in(&runtime_dir()?))
}
fn lock_path_in(runtime: &Path) -> PathBuf {
runtime.join("eqtui").join("eqtui.lock")
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn temp_dir(tag: &str, mode: u32) -> PathBuf {
let base = std::env::temp_dir().join(format!(
"eqtui-paths-{}-{}-{tag}",
std::process::id(),
tag.len()
));
let dir = base;
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir(&dir).unwrap();
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(mode)).unwrap();
dir
}
fn cleanup(dir: &Path) {
let _ = std::fs::remove_dir_all(dir);
}
#[test]
fn owned_private_dir_is_valid() {
let euid = unsafe { libc::geteuid() };
let dir = temp_dir("owned-0700", 0o700);
let result = validate_dir(&dir, euid);
assert!(
result.is_ok(),
"0700 dir owned by euid should validate: {result:?}"
);
cleanup(&dir);
}
#[test]
fn group_accessible_dir_is_rejected() {
let euid = unsafe { libc::geteuid() };
let dir = temp_dir("group-0755", 0o755);
let result = validate_dir(&dir, euid);
assert!(result.is_err(), "0755 dir must be rejected");
cleanup(&dir);
}
#[test]
fn missing_dir_is_rejected() {
let euid = unsafe { libc::geteuid() };
let dir = std::env::temp_dir().join(format!("eqtui-paths-missing-{}", std::process::id()));
let result = validate_dir(&dir, euid);
assert!(result.is_err(), "missing dir must be rejected");
}
#[test]
fn regular_file_is_rejected() {
let euid = unsafe { libc::geteuid() };
let file = std::env::temp_dir().join(format!("eqtui-paths-file-{}", std::process::id()));
std::fs::write(&file, b"not a dir").unwrap();
let result = validate_dir(&file, euid);
assert!(result.is_err(), "a regular file must be rejected");
let _ = std::fs::remove_file(&file);
}
#[test]
fn wrong_owner_is_rejected_when_root() {
if unsafe { libc::geteuid() } != 0 {
return;
}
let dir = temp_dir("foreign-0700", 0o700);
let result = validate_dir(&dir, 1);
assert!(
result.is_err(),
"dir not owned by expected uid must be rejected"
);
cleanup(&dir);
}
#[test]
fn socket_path_uses_private_subdir() {
let fake_runtime = Path::new("/tmp/fake-runtime");
let path = socket_path_in(fake_runtime);
assert_eq!(path, Path::new("/tmp/fake-runtime/eqtui/eqtui.sock"));
}
#[test]
fn lock_path_uses_private_subdir() {
let fake_runtime = Path::new("/tmp/fake-runtime");
let path = lock_path_in(fake_runtime);
assert_eq!(path, Path::new("/tmp/fake-runtime/eqtui/eqtui.lock"));
}
}