90 lines
2.8 KiB
Rust
90 lines
2.8 KiB
Rust
// 本机会话持久化:令牌只存本机数据目录(0600),不进入 WebView 或日志。
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::api::Line;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Session {
|
|
pub base_url: String,
|
|
pub token: String,
|
|
pub username: String,
|
|
pub display_name: String,
|
|
pub device_name: String,
|
|
pub lines: Vec<Line>,
|
|
}
|
|
|
|
fn session_path(dir: &Path) -> PathBuf {
|
|
dir.join("session.json")
|
|
}
|
|
|
|
pub fn save(dir: &Path, session: &Session) -> Result<(), String> {
|
|
std::fs::create_dir_all(dir).map_err(|err| format!("无法创建数据目录:{err}"))?;
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
|
|
.map_err(|err| format!("无法限制数据目录权限:{err}"))?;
|
|
}
|
|
let raw = serde_json::to_string_pretty(session).map_err(|err| err.to_string())?;
|
|
write_private(&session_path(dir), raw.as_bytes())
|
|
}
|
|
|
|
pub fn load(dir: &Path) -> Option<Session> {
|
|
let raw = std::fs::read_to_string(session_path(dir)).ok()?;
|
|
serde_json::from_str(&raw).ok()
|
|
}
|
|
|
|
pub fn clear(dir: &Path) {
|
|
let _ = std::fs::remove_file(session_path(dir));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
fn write_private(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
|
use std::io::Write;
|
|
use std::os::unix::fs::OpenOptionsExt;
|
|
let mut file = std::fs::OpenOptions::new()
|
|
.write(true)
|
|
.create(true)
|
|
.truncate(true)
|
|
.mode(0o600)
|
|
.open(path)
|
|
.map_err(|err| format!("无法写入会话文件:{err}"))?;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
file.set_permissions(std::fs::Permissions::from_mode(0o600))
|
|
.map_err(|err| format!("无法限制会话文件权限:{err}"))?;
|
|
file.write_all(bytes)
|
|
.map_err(|err| format!("写入会话文件失败:{err}"))
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn write_private(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
|
std::fs::write(path, bytes).map_err(|err| format!("无法写入会话文件:{err}"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn session_roundtrip_and_clear() {
|
|
let dir = std::env::temp_dir().join(format!("opsx-session-test-{}", std::process::id()));
|
|
let session = Session {
|
|
base_url: "https://vpn.example.com".to_string(),
|
|
token: "token-abc".to_string(),
|
|
username: "alice".to_string(),
|
|
display_name: "爱丽丝".to_string(),
|
|
device_name: "alice-mac".to_string(),
|
|
lines: vec![],
|
|
};
|
|
save(&dir, &session).unwrap();
|
|
let loaded = load(&dir).unwrap();
|
|
assert_eq!(loaded.token, "token-abc");
|
|
assert_eq!(loaded.username, "alice");
|
|
clear(&dir);
|
|
assert!(load(&dir).is_none());
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
}
|