use std::path::PathBuf;
use crate::session::SessionBackend;
pub(crate) struct FileBackend {
pub(crate) sessions_dir: PathBuf,
pub(crate) reason: &'static str,
}
impl FileBackend {
fn sessions_path(&self) -> PathBuf {
self.sessions_dir.join("sessions.json")
}
fn notice_once(&self) {
use std::sync::Once;
static NOTICE: Once = Once::new();
let reason = self.reason;
let path = self.sessions_path();
NOTICE.call_once(|| {
eprintln!(
"note: sessions are stored as plaintext at mode 0600 in {} \
(selected by {reason}).",
path.display()
);
});
}
fn load_map(&self) -> std::collections::HashMap<String, serde_json::Value> {
let path = self.sessions_path();
let data = match std::fs::read_to_string(&path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return std::collections::HashMap::new();
}
Err(e) => {
tracing::warn!("failed to read sessions file {}: {e}", path.display());
return std::collections::HashMap::new();
}
};
match serde_json::from_str(&data) {
Ok(map) => map,
Err(e) => {
tracing::warn!("failed to parse sessions file {}: {e}", path.display());
std::collections::HashMap::new()
}
}
}
fn save_map(
&self,
map: &std::collections::HashMap<String, serde_json::Value>,
) -> Result<(), Box<dyn std::error::Error>> {
let path = self.sessions_path();
let json = serde_json::to_string_pretty(map)?;
create_owner_only(&path)?;
std::fs::write(&path, json)?;
Ok(())
}
}
fn create_dir_owner_only(dir: &std::path::Path) -> std::io::Result<()> {
std::fs::create_dir_all(dir)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = std::fs::metadata(dir)?.permissions();
perms.set_mode(0o700);
std::fs::set_permissions(dir, perms)?;
}
Ok(())
}
fn create_owner_only(path: &std::path::Path) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
create_dir_owner_only(parent)?;
}
#[cfg(unix)]
{
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.mode(0o600)
.open(path)?;
let mut perms = std::fs::metadata(path)?.permissions();
perms.set_mode(0o600);
std::fs::set_permissions(path, perms)?;
}
#[cfg(not(unix))]
{
if !path.exists() {
std::fs::File::create(path)?;
}
}
Ok(())
}
impl SessionBackend for FileBackend {
fn load(&self, key: &str) -> Option<String> {
self.notice_once();
let map = self.load_map();
map.get(key).map(|v| v.to_string())
}
fn save(&self, key: &str, value: &str) -> Result<(), Box<dyn std::error::Error>> {
self.notice_once();
let mut map = self.load_map();
let parsed: serde_json::Value = serde_json::from_str(value)?;
map.insert(key.to_string(), parsed);
self.save_map(&map)
}
fn clear(&self, key: &str) {
let mut map = self.load_map();
map.remove(key);
let _ = self.save_map(&map);
}
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
fn mode_of(p: &std::path::Path) -> u32 {
std::fs::metadata(p).unwrap().permissions().mode() & 0o777
}
#[test]
fn a_saved_session_is_owner_only() {
let dir = std::env::temp_dir().join(format!("vta-sdk-file-backend-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let backend = FileBackend {
sessions_dir: dir.clone(),
reason: "a test",
};
backend.save("k", r#"{"private_key":"secret"}"#).unwrap();
assert_eq!(
mode_of(&backend.sessions_path()),
0o600,
"file must be 0600"
);
assert_eq!(mode_of(&dir), 0o700, "directory must be 0700");
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn an_existing_world_readable_file_is_repaired() {
let dir = std::env::temp_dir().join(format!(
"vta-sdk-file-backend-legacy-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("sessions.json");
std::fs::write(&path, "{}").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644)).unwrap();
assert_eq!(mode_of(&path), 0o644, "precondition: starts world-readable");
let backend = FileBackend {
sessions_dir: dir.clone(),
reason: "a test",
};
backend.save("k", r#"{"private_key":"secret"}"#).unwrap();
assert_eq!(
mode_of(&path),
0o600,
"an existing file must be re-hardened"
);
std::fs::remove_dir_all(&dir).unwrap();
}
}