use crate::paths::default_ipc_token_path;
use crate::{ProtocolError, Result};
use rand::{rngs::OsRng, RngCore};
use std::io::Write;
use std::path::{Path, PathBuf};
use tracing::warn;
use zeroize::Zeroize;
pub fn load_ipc_token() -> Result<String> {
load_ipc_token_from(&default_ipc_token_path())
}
pub fn native_host_capability_path() -> PathBuf {
crate::paths::get_config_dir().join("native_host.capability")
}
pub fn load_native_host_capability() -> Option<String> {
std::fs::read_to_string(native_host_capability_path())
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
pub fn load_ipc_token_from(token_path: &Path) -> Result<String> {
let token = std::fs::read_to_string(token_path)?.trim().to_string();
if token.is_empty() {
return Err(ProtocolError::Ipc(format!(
"IPC token file is empty: {:?}",
token_path
)));
}
enforce_owner_only(token_path);
Ok(token)
}
pub fn load_or_create_ipc_token() -> Result<String> {
load_or_create_ipc_token_at(&default_ipc_token_path())
}
pub fn load_or_create_ipc_token_at(token_path: &Path) -> Result<String> {
if let Some(parent) = token_path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
}
} else {
std::fs::create_dir_all(parent)?;
}
}
if token_path.exists() {
return load_ipc_token_from(token_path);
}
let mut token_bytes = [0u8; 32];
OsRng.fill_bytes(&mut token_bytes);
let token = hex::encode(token_bytes);
token_bytes.zeroize();
let mut options = std::fs::OpenOptions::new();
options.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.mode(0o600);
}
let mut file = options.open(token_path)?;
file.write_all(token.as_bytes())?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600))?;
}
Ok(token)
}
fn enforce_owner_only(token_path: &Path) {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let owner_only = std::fs::metadata(token_path)
.map(|m| m.permissions().mode() & 0o077 == 0)
.unwrap_or(true); if !owner_only {
warn!(
"IPC token file {:?} is group/world-accessible; tightening to 0600",
token_path
);
let _ = std::fs::set_permissions(token_path, std::fs::Permissions::from_mode(0o600));
}
}
#[cfg(not(unix))]
{
let _ = token_path;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn created_token_and_parent_are_owner_only() {
use std::os::unix::fs::PermissionsExt;
let outer = tempfile::TempDir::new().unwrap();
let token_path = outer.path().join("cfg").join("ipc.token");
load_or_create_ipc_token_at(&token_path).unwrap();
let mode = |p: &Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777;
assert_eq!(mode(&token_path), 0o600, "token must be born 0600");
assert_eq!(
mode(token_path.parent().unwrap()),
0o700,
"created parent dir must be 0700"
);
let again = load_or_create_ipc_token_at(&token_path).unwrap();
let first = std::fs::read_to_string(&token_path).unwrap();
assert_eq!(again.trim(), first.trim());
}
#[cfg(unix)]
#[test]
fn loose_token_mode_is_repaired_on_load() {
use std::os::unix::fs::PermissionsExt;
let outer = tempfile::TempDir::new().unwrap();
let token_path = outer.path().join("ipc.token");
load_or_create_ipc_token_at(&token_path).unwrap();
std::fs::set_permissions(&token_path, std::fs::Permissions::from_mode(0o644)).unwrap();
load_ipc_token_from(&token_path).unwrap();
let mode = std::fs::metadata(&token_path).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600, "load must tighten a loose token mode");
}
#[cfg(unix)]
#[test]
fn preexisting_parent_directory_is_left_untouched() {
use std::os::unix::fs::PermissionsExt;
let outer = tempfile::TempDir::new().unwrap(); let token_path = outer.path().join("ipc.token");
load_or_create_ipc_token_at(&token_path).unwrap();
let loose = outer.path().join("loose");
std::fs::create_dir_all(&loose).unwrap();
std::fs::set_permissions(&loose, std::fs::Permissions::from_mode(0o755)).unwrap();
let token_path = loose.join("ipc.token");
load_or_create_ipc_token_at(&token_path).unwrap();
let mode = std::fs::metadata(&loose).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o755, "pre-existing parent must stay untouched");
}
}