use std::fs;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use anyhow::bail;
use anyhow::{Context, Result};
use rand_core::{OsRng, RngCore};
pub const TOKEN_FILENAME: &str = "token";
pub fn default_daemon_dir() -> Result<PathBuf> {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
.context("Unable to resolve home directory for daemon state")?;
Ok(home.join(".vetto").join("daemon"))
}
pub fn ensure_daemon_dir(dir: &Path) -> Result<()> {
fs::create_dir_all(dir).with_context(|| format!("failed to create {}", dir.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = fs::Permissions::from_mode(0o700);
let _ = fs::set_permissions(dir, perms);
}
Ok(())
}
pub fn ensure_daemon_token(dir: &Path) -> Result<String> {
let token_path = dir.join(TOKEN_FILENAME);
if token_path.is_file() {
let raw = fs::read_to_string(&token_path).with_context(|| {
format!("failed to read daemon token from {}", token_path.display())
})?;
let trimmed = raw.trim().to_string();
if !trimmed.is_empty() {
return Ok(trimmed);
}
}
let mut random_bytes = [0u8; 32];
OsRng.fill_bytes(&mut random_bytes);
let mut token = String::with_capacity(64);
for b in random_bytes {
use std::fmt::Write;
let _ = write!(token, "{:02x}", b);
}
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut opts = fs::OpenOptions::new();
opts.write(true).create(true).truncate(true).mode(0o600);
let mut file = opts
.open(&token_path)
.with_context(|| format!("failed to create token at {}", token_path.display()))?;
use std::io::Write;
file.write_all(token.as_bytes())?;
}
#[cfg(not(unix))]
{
fs::write(&token_path, token.as_bytes())
.with_context(|| format!("failed to create token at {}", token_path.display()))?;
}
Ok(token)
}
#[cfg(unix)]
pub fn verify_peer_cred(stream: &std::os::unix::net::UnixStream) -> Result<()> {
#[cfg(target_os = "linux")]
{
use std::os::fd::AsRawFd;
let fd = stream.as_raw_fd();
let mut ucred = libc::ucred {
pid: 0,
uid: u32::MAX,
gid: u32::MAX,
};
let mut len = std::mem::size_of::<libc::ucred>() as libc::socklen_t;
let ret = unsafe {
libc::getsockopt(
fd,
libc::SOL_SOCKET,
libc::SO_PEERCRED,
&mut ucred as *mut _ as *mut libc::c_void,
&mut len,
)
};
if ret != 0 {
bail!(
"failed to get SO_PEERCRED on Unix socket: {}",
std::io::Error::last_os_error()
);
}
let my_uid = unsafe { libc::geteuid() };
if ucred.uid != my_uid {
bail!(
"unauthorized peer UID {} (expected daemon owner UID {})",
ucred.uid,
my_uid
);
}
Ok(())
}
#[cfg(target_os = "macos")]
{
use std::os::fd::AsRawFd;
let fd = stream.as_raw_fd();
let mut uid: libc::uid_t = 0;
let mut gid: libc::gid_t = 0;
let ret = unsafe { libc::getpeereid(fd, &mut uid, &mut gid) };
if ret != 0 {
bail!(
"failed to getpeereid on Unix socket: {}",
std::io::Error::last_os_error()
);
}
let my_uid = unsafe { libc::geteuid() };
if uid != my_uid {
bail!(
"unauthorized peer UID {} (expected daemon owner UID {})",
uid,
my_uid
);
}
Ok(())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = stream;
Ok(())
}
}
#[cfg(not(unix))]
pub fn verify_peer_cred<T>(_stream: &T) -> Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_token_generation_and_length() {
let dir = std::env::temp_dir().join(format!("vetto-auth-test-{}", std::process::id()));
let _ = fs::remove_dir_all(&dir);
ensure_daemon_dir(&dir).unwrap();
let token = ensure_daemon_token(&dir).unwrap();
assert_eq!(token.len(), 64);
let token_again = ensure_daemon_token(&dir).unwrap();
assert_eq!(token, token_again);
let _ = fs::remove_dir_all(&dir);
}
}