- Remove CLI options except --config and --test-processing-delay - Enforce 1026 byte request limit per Gemini spec (1024 + 2 for CRLF) - Add comprehensive URL parsing with host and port validation - Reject malformed URIs and wrong ports with 59 Bad Request - Update tests for new URL parsing signature - Fix clippy warning in port parsing
165 lines
No EOL
5.3 KiB
Rust
165 lines
No EOL
5.3 KiB
Rust
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<String, ()> {
|
|
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::<u16>().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<PathBuf, PathResolutionError> {
|
|
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");
|
|
}
|
|
} |