//! Resolves a finger username to a response: the contents of that user's //! plan file if one exists and is readable, or the username echoed back //! unchanged otherwise. Input is validated first so a request can never walk //! outside the configured plan-file directory. use std::path::{Path, PathBuf}; pub trait FilesystemWrapper { fn exists(&self, path: &Path) -> bool; fn read_file(&self, path: &Path) -> String; } pub struct RealFilesystemWrapper; impl FilesystemWrapper for RealFilesystemWrapper { fn exists(&self, path: &Path) -> bool { path.exists() } fn read_file(&self, path: &Path) -> String { let raw = match std::fs::read_to_string(path) { Ok(c) => c, Err(_) => return String::new(), }; if raw.is_empty() { return String::new(); } // Plan files are returned over the finger protocol, which expects // CRLF line endings; normalise whatever the file used to LF-joined // lines terminated by a single CRLF. let body = raw.lines().collect::>().join("\n"); format!("{body}\r\n") } } pub const BASE_PATH: &str = "/var/finger/users/"; pub fn process(username: &str) -> String { let fs = RealFilesystemWrapper; process_with(username, &fs, Path::new(BASE_PATH)) } pub fn process_with(username: &str, fs: &dyn FilesystemWrapper, basepath: &Path) -> String { const TRAVERSAL_PATTERNS: [&str; 10] = [ "../", "..\\", "%2e%2e%2f", "%2e%2e%5c", "%2E%2E%2F", "%2E%2E%5C", "..%2f", "..%5c", "..%2F", "..%5C", ]; if TRAVERSAL_PATTERNS.iter().any(|p| username.contains(p)) { return "InvalidInput: Directory traversal detected in username\r\n".to_string(); } if username.contains('/') { return "InvalidInput: Path detected in username\r\n".to_string(); } // Plan-file lookup is case-insensitive: normalise the requested name to // lower-case so e.g. "Pete" resolves the on-disk "pete" plan. Plan // filenames are always lower-case; the original spelling is still echoed // back below when no plan exists. Lower-casing is ASCII-only to match // the byte-wise ::tolower behavior of the reference implementation. let lookup = username.to_ascii_lowercase(); let plan_path: PathBuf = basepath.join(lookup); if !fs.exists(&plan_path) { return username.to_string(); } let content = fs.read_file(&plan_path); if content.is_empty() { return username.to_string(); } content } #[cfg(test)] mod tests { use super::*; use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; #[test] fn valid_username_simple() { assert_eq!(process("john"), "john"); } #[test] fn valid_username_with_numbers() { assert_eq!(process("user123"), "user123"); } #[test] fn valid_username_with_underscore() { assert_eq!(process("user_name"), "user_name"); } #[test] fn valid_username_with_hyphen() { assert_eq!(process("user-name"), "user-name"); } #[test] fn valid_username_empty_string() { assert_eq!(process(""), ""); } #[test] fn directory_traversal_basic_dot_dot_slash() { let result = process("user../file"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_basic_dot_dot_backslash() { let result = process("user..\\file"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_url_encoded_lowercase_2e2e2f() { let result = process("user%2e%2e%2ffile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_url_encoded_lowercase_2e2e5c() { let result = process("user%2e%2e%5cfile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_url_encoded_uppercase_2e2e2f() { let result = process("user%2E%2E%2Ffile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_url_encoded_uppercase_2e2e5c() { let result = process("user%2E%2E%5Cfile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_mixed_dot_dot_2f() { let result = process("user..%2ffile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_mixed_dot_dot_5c() { let result = process("user..%5cfile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_mixed_dot_dot_2f_upper() { let result = process("user..%2Ffile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn directory_traversal_mixed_dot_dot_5c_upper() { let result = process("user..%5Cfile"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn path_detection_forward_slash() { let result = process("user/name"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Path detected")); } #[test] fn path_detection_forward_slash_at_start() { let result = process("/username"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Path detected")); } #[test] fn path_detection_forward_slash_at_end() { let result = process("username/"); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Path detected")); } #[test] fn single_dot() { assert_eq!(process("."), "."); } #[test] fn double_dot_without_slash() { assert_eq!(process(".."), ".."); } #[test] fn contains_dot_but_not_traversal() { assert_eq!(process("user.name"), "user.name"); } #[test] fn backslash_without_dots() { assert_eq!(process("user\\name"), "user\\name"); } // --- Fake-filesystem tests (mirrors test_handler_mock.cpp) --- #[derive(Default)] struct FakeFilesystem { files: HashMap, } impl FakeFilesystem { fn with_file(path: impl Into, content: impl Into) -> Self { let mut files = HashMap::new(); files.insert(path.into(), content.into()); FakeFilesystem { files } } } impl FilesystemWrapper for FakeFilesystem { fn exists(&self, path: &Path) -> bool { self.files.contains_key(path) } fn read_file(&self, path: &Path) -> String { self.files.get(path).cloned().unwrap_or_default() } } #[test] fn process_with_file_exists() { let base = Path::new("/var/finger/users/"); let fs = FakeFilesystem::with_file(base.join("testuser"), "Mock file content\r\n"); assert_eq!( process_with("testuser", &fs, base), "Mock file content\r\n" ); } #[test] fn process_with_file_not_found() { let base = Path::new("/var/finger/users/"); let fs = FakeFilesystem::default(); assert_eq!(process_with("nonexistentuser", &fs, base), "nonexistentuser"); } #[test] fn process_with_empty_file() { let base = Path::new("/var/finger/users/"); let fs = FakeFilesystem::with_file(base.join("emptyfileuser"), ""); assert_eq!(process_with("emptyfileuser", &fs, base), "emptyfileuser"); } #[test] fn process_lowercases_username_for_lookup() { let base = Path::new("/var/finger/users/"); let fs = FakeFilesystem::with_file(base.join("pete"), "Just another hacker.\r\n"); assert_eq!( process_with("Pete", &fs, base), "Just another hacker.\r\n" ); } // --- Real-filesystem tests (mirrors test_handler_real_filesystem.cpp) --- static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); struct TempDir(PathBuf); impl TempDir { fn new() -> Self { let n = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); let dir = std::env::temp_dir().join(format!( "finger_rs_test_{}_{n}", std::process::id() )); std::fs::create_dir_all(&dir).unwrap(); TempDir(dir) } fn path(&self) -> &Path { &self.0 } } impl Drop for TempDir { fn drop(&mut self) { let _ = std::fs::remove_dir_all(&self.0); } } fn write_file(dir: &Path, name: &str, content: &str) { std::fs::write(dir.join(name), content).unwrap(); } #[test] fn real_fs_exists_with_real_file() { let dir = TempDir::new(); write_file(dir.path(), "testuser", "Test content"); let fs = RealFilesystemWrapper; assert!(fs.exists(&dir.path().join("testuser"))); } #[test] fn real_fs_exists_with_nonexistent_file() { let dir = TempDir::new(); let fs = RealFilesystemWrapper; assert!(!fs.exists(&dir.path().join("nonexistent"))); } #[test] fn real_fs_read_file_with_simple_content() { let dir = TempDir::new(); write_file(dir.path(), "simple", "Hello, World!"); let fs = RealFilesystemWrapper; assert_eq!( fs.read_file(&dir.path().join("simple")), "Hello, World!\r\n" ); } #[test] fn real_fs_read_file_with_multiline_content() { let dir = TempDir::new(); write_file(dir.path(), "multiline", "Line 1\nLine 2\nLine 3"); let fs = RealFilesystemWrapper; assert_eq!( fs.read_file(&dir.path().join("multiline")), "Line 1\nLine 2\nLine 3\r\n" ); } #[test] fn real_fs_read_file_with_empty_file() { let dir = TempDir::new(); write_file(dir.path(), "empty", ""); let fs = RealFilesystemWrapper; assert_eq!(fs.read_file(&dir.path().join("empty")), ""); } #[test] fn real_fs_read_file_nonexistent_file() { let dir = TempDir::new(); let fs = RealFilesystemWrapper; assert_eq!(fs.read_file(&dir.path().join("nonexistent")), ""); } #[test] fn process_with_existing_user_file() { let dir = TempDir::new(); write_file( dir.path(), "johndoe", "John Doe\nSoftware Engineer\nLoves Rust", ); let fs = RealFilesystemWrapper; assert_eq!( process_with("johndoe", &fs, dir.path()), "John Doe\nSoftware Engineer\nLoves Rust\r\n" ); } #[test] fn process_with_nonexistent_user_file() { let dir = TempDir::new(); let fs = RealFilesystemWrapper; assert_eq!(process_with("nonexistentuser", &fs, dir.path()), "nonexistentuser"); } #[test] fn process_with_empty_user_file() { let dir = TempDir::new(); write_file(dir.path(), "emptyuser", ""); let fs = RealFilesystemWrapper; assert_eq!(process_with("emptyuser", &fs, dir.path()), "emptyuser"); } #[test] fn process_with_file_containing_only_newlines() { let dir = TempDir::new(); write_file(dir.path(), "newlineuser", "\n\n\n"); let fs = RealFilesystemWrapper; // Line-by-line reading means the trailing empty line after the final // \n is not read as a separate line, resulting in "\n\n\r\n". assert_eq!(process_with("newlineuser", &fs, dir.path()), "\n\n\r\n"); } #[test] fn process_directory_traversal_protection_with_real_fs() { let dir = TempDir::new(); let result = process_with("../secret", &RealFilesystemWrapper, dir.path()); assert!(result.starts_with("InvalidInput:")); assert!(result.contains("Directory traversal detected")); } #[test] fn process_with_custom_base_path() { let dir = TempDir::new(); let custom_base = dir.path().join("custom_users"); std::fs::create_dir_all(&custom_base).unwrap(); write_file(&custom_base, "customuser", "Custom base path user"); let fs = RealFilesystemWrapper; assert_eq!( process_with("customuser", &fs, &custom_base), "Custom base path user\r\n" ); } #[test] fn real_fs_read_file_with_special_characters() { let dir = TempDir::new(); let content = "User with special chars: \u{e0}\u{e1}\u{e2}\u{e3}\u{e4}\u{e5}\u{e6}\u{e7}\u{e8}\u{e9}\u{ea}\u{eb}"; write_file(dir.path(), "specialuser", content); let fs = RealFilesystemWrapper; assert_eq!( fs.read_file(&dir.path().join("specialuser")), format!("{content}\r\n") ); } }