use path_security::validate_path; use std::path::{Path, PathBuf}; #[derive(Debug, PartialEq)] pub enum PathResolutionError { NotFound, } pub fn parse_gemini_url(request: &str, expected_host: &str, expected_port: u16) -> Result { if let Some(url) = request.strip_prefix("gemini://") { let host_port_end = url.find('/').unwrap_or(url.len()); let host_port = &url[..host_port_end]; // Parse host and port let (host, port_str) = if let Some(colon_pos) = host_port.find(':') { let host = &host_port[..colon_pos]; let port_str = &host_port[colon_pos + 1..]; (host, Some(port_str)) } else { (host_port, None) }; // Validate host if host != expected_host { return Err(()); // Hostname mismatch } // Validate port let port = port_str .and_then(|p| p.parse::().ok()) .unwrap_or(1965); if port != expected_port { return Err(()); // Port mismatch } let path = if host_port_end < url.len() { &url[host_port_end..] } else { "/" }; Ok(path.trim().to_string()) } else { Err(()) } } pub fn resolve_file_path(path: &str, dir: &str) -> Result { let file_path_str = if path == "/" { "index.gmi".to_string() } else if path.ends_with('/') { format!("{}index.gmi", &path[1..]) } else { path[1..].to_string() }; match validate_path(Path::new(&file_path_str), Path::new(dir)) { Ok(safe_path) => { // Path is secure, now check if file exists if safe_path.exists() { Ok(safe_path) } else { Err(PathResolutionError::NotFound) } }, Err(_) => { // Path validation failed - treat as not found Err(PathResolutionError::NotFound) }, } } pub fn get_mime_type(file_path: &Path) -> &str { if let Some(ext) = file_path.extension() { match ext.to_str() { Some("gmi") => "text/gemini", Some("txt") => "text/plain", Some("html") => "text/html", Some("png") => "image/png", Some("jpg") | Some("jpeg") => "image/jpeg", Some("webp") => "image/webp", Some("gif") => "image/gif", _ => "application/octet-stream", } } else { "application/octet-stream" } } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; #[test] fn test_parse_gemini_url_valid() { assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/", "gemini.jeena.net", 1965), Ok("/".to_string())); assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/posts/test", "gemini.jeena.net", 1965), Ok("/posts/test".to_string())); } #[test] fn test_parse_gemini_url_invalid_host() { assert!(parse_gemini_url("gemini://foo.com/", "gemini.jeena.net", 1965).is_err()); } #[test] fn test_parse_gemini_url_no_prefix() { assert!(parse_gemini_url("http://gemini.jeena.net/", "gemini.jeena.net", 1965).is_err()); } #[test] fn test_resolve_file_path_root() { let temp_dir = TempDir::new().unwrap(); // Create index.gmi file since we now check for existence std::fs::write(temp_dir.path().join("index.gmi"), "# Test").unwrap(); assert!(resolve_file_path("/", temp_dir.path().to_str().unwrap()).is_ok()); } #[test] fn test_resolve_file_path_directory() { let temp_dir = TempDir::new().unwrap(); std::fs::create_dir(temp_dir.path().join("test")).unwrap(); std::fs::write(temp_dir.path().join("test/index.gmi"), "# Test").unwrap(); assert!(resolve_file_path("/test/", temp_dir.path().to_str().unwrap()).is_ok()); } #[test] fn test_resolve_file_path_file() { let temp_dir = TempDir::new().unwrap(); std::fs::write(temp_dir.path().join("test.gmi"), "# Test").unwrap(); assert!(resolve_file_path("/test.gmi", temp_dir.path().to_str().unwrap()).is_ok()); } #[test] fn test_resolve_file_path_traversal() { let temp_dir = TempDir::new().unwrap(); assert_eq!(resolve_file_path("/../etc/passwd", temp_dir.path().to_str().unwrap()), Err(PathResolutionError::NotFound)); } #[test] fn test_resolve_file_path_not_found() { let temp_dir = TempDir::new().unwrap(); // Don't create the file, should return NotFound error assert_eq!(resolve_file_path("/nonexistent.gmi", temp_dir.path().to_str().unwrap()), Err(PathResolutionError::NotFound)); } #[test] fn test_get_mime_type_gmi() { let path = Path::new("test.gmi"); assert_eq!(get_mime_type(path), "text/gemini"); } #[test] fn test_get_mime_type_png() { let path = Path::new("test.png"); assert_eq!(get_mime_type(path), "image/png"); } #[test] fn test_get_mime_type_unknown() { let path = Path::new("test.xyz"); assert_eq!(get_mime_type(path), "application/octet-stream"); } #[test] fn test_get_mime_type_no_extension() { let path = Path::new("test"); assert_eq!(get_mime_type(path), "application/octet-stream"); } }