use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
use tokio::io::AsyncWriteExt;
use tokio::net::{UnixListener, UnixStream};
use crate::error::{NdsError, Result};
use crate::session::Session;
#[allow(dead_code)]
pub const DEFAULT_BUFFER_SIZE: usize = 16384; #[allow(dead_code)]
pub const SMALL_BUFFER_SIZE: usize = 4096;
#[allow(dead_code)]
pub async fn create_listener_async(session_id: &str) -> Result<(UnixListener, PathBuf)> {
let socket_path = Session::socket_dir()?.join(format!("{}.sock", session_id));
if socket_path.exists() {
tokio::fs::remove_file(&socket_path).await?;
}
let listener = UnixListener::bind(&socket_path)
.map_err(|e| NdsError::SocketError(format!("Failed to bind socket: {}", e)))?;
let metadata = tokio::fs::metadata(&socket_path).await?;
let mut permissions = metadata.permissions();
permissions.set_mode(0o600);
tokio::fs::set_permissions(&socket_path, permissions).await?;
Ok((listener, socket_path))
}
#[allow(dead_code)]
pub async fn send_resize_command_async(
socket: &mut UnixStream,
cols: u16,
rows: u16,
) -> tokio::io::Result<()> {
let cols = sanitize_numeric_input(cols);
let rows = sanitize_numeric_input(rows);
let resize_cmd = format!("\x1b]nds:resize:{}:{}\x07", cols, rows);
socket.write_all(resize_cmd.as_bytes()).await?;
socket.flush().await
}
pub fn sanitize_numeric_input(value: u16) -> u16 {
value.min(9999).max(1)
}
pub fn sanitize_string_input(input: &str) -> String {
input
.chars()
.filter(|c| !c.is_control() || *c == '\n' || *c == '\r' || *c == '\t')
.take(4096) .collect()
}
pub fn parse_nds_command_secure(data: &[u8]) -> Option<(String, Vec<String>)> {
if data.len() > 10 && data.len() < 8192 && data.starts_with(b"\x1b]nds:") {
if let Ok(cmd_str) = std::str::from_utf8(data) {
if let Some(end_idx) = cmd_str.find('\x07') {
let cmd = &cmd_str[6..end_idx];
if !is_valid_command(cmd) {
return None;
}
let parts: Vec<String> = cmd.split(':').map(|s| sanitize_string_input(s)).collect();
if !parts.is_empty() && parts.len() <= 10 {
return Some((parts[0].clone(), parts[1..].to_vec()));
}
}
}
}
None
}
pub fn is_valid_command(cmd: &str) -> bool {
const ALLOWED_COMMANDS: &[&str] = &[
"resize",
"detach",
"attach",
"list",
"kill",
"switch",
"scrollback",
"clear",
"refresh",
];
if let Some(command) = cmd.split(':').next() {
ALLOWED_COMMANDS.contains(&command)
} else {
false
}
}
#[allow(dead_code)]
pub fn get_command_end_secure(data: &[u8]) -> Option<usize> {
if data.len() < 8192 && data.starts_with(b"\x1b]nds:") {
if let Ok(cmd_str) = std::str::from_utf8(data) {
if let Some(end_idx) = cmd_str.find('\x07') {
if end_idx < 1024 {
return Some(end_idx + 1);
}
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sanitize_numeric_input() {
assert_eq!(sanitize_numeric_input(100), 100);
assert_eq!(sanitize_numeric_input(10000), 9999);
assert_eq!(sanitize_numeric_input(0), 1);
}
#[test]
fn test_sanitize_string_input() {
assert_eq!(sanitize_string_input("hello"), "hello");
assert_eq!(sanitize_string_input("hello\x00world"), "helloworld");
assert_eq!(sanitize_string_input("hello\nworld"), "hello\nworld");
}
#[test]
fn test_is_valid_command() {
assert!(is_valid_command("resize:80:24"));
assert!(is_valid_command("detach"));
assert!(!is_valid_command("rm:rf:/"));
assert!(!is_valid_command("unknown:command"));
}
}