From 55fe47b172531ac1468ab0511a90791599755305 Mon Sep 17 00:00:00 2001 From: Jeena Date: Thu, 22 Jan 2026 05:25:46 +0000 Subject: [PATCH] Replace custom logging with tracing crate and RUST_LOG env var - Remove custom logging module and init_logging function - Update main.rs to use tracing_subscriber with EnvFilter - Remove log_level from global config structure - Update documentation and tests to use RUST_LOG - Format long lines in config.rs and test files for better readability --- README.md | 19 ++- dist/INSTALL.md | 20 ++- src/config.rs | 83 +++++++++--- src/logging.rs | 107 +-------------- src/main.rs | 144 ++++++++++++++------ src/request.rs | 52 +++++--- src/server.rs | 25 ++-- src/tls.rs | 7 +- tests/common.rs | 40 ++++-- tests/config_validation.rs | 214 ++++++++++++++++++------------ tests/rate_limiting.rs | 41 ++++-- tests/virtual_host_config.rs | 154 +++++++++++++-------- tests/virtual_host_integration.rs | 178 ++++++++++++++++++------- tests/virtual_host_paths.rs | 82 ++++++++---- tests/virtual_host_routing.rs | 80 +++++++---- 15 files changed, 787 insertions(+), 459 deletions(-) diff --git a/README.md b/README.md index e2d8722..24db44b 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,26 @@ Access with a Gemini client like Lagrange at `gemini://yourdomain.com/`. ## Options - `--config` (`-C`): Path to config file (default `/etc/pollux/config.toml`) -- `--no-tls`: Disable TLS for testing (uses raw TCP connections) - `--test-processing-delay` (debug builds only): Add delay before processing requests (seconds) - for testing rate limiting +## Logging + +Pollux uses the `tracing` crate for structured logging. Configure log levels with the `RUST_LOG` environment variable: + +```bash +# Basic usage +export RUST_LOG=info +./pollux + +# Module-specific levels +export RUST_LOG=pollux=debug,sqlx=info + +# Maximum verbosity +export RUST_LOG=trace +``` + +Available levels: `error`, `warn`, `info`, `debug`, `trace` + ## Security Pollux is designed with security as a priority: diff --git a/dist/INSTALL.md b/dist/INSTALL.md index bbc2579..0d79719 100644 --- a/dist/INSTALL.md +++ b/dist/INSTALL.md @@ -104,7 +104,6 @@ Edit `/etc/pollux/config.toml`: bind_host = "0.0.0.0" port = 1965 max_concurrent_requests = 1000 -log_level = "info" # Host configuration ["example.com"] @@ -113,6 +112,22 @@ cert = "/etc/pollux/tls/cert.pem" key = "/etc/pollux/tls/key.pem" ``` +### Logging Configuration + +Pollux uses structured logging with the `tracing` crate. Configure log levels using the `RUST_LOG` environment variable: + +```bash +# Set log level before starting the service +export RUST_LOG=info +sudo systemctl start pollux + +# Or for debugging +export RUST_LOG=pollux=debug +sudo systemctl restart pollux + +# Available levels: error, warn, info, debug, trace +``` + ### Content Setup ```bash @@ -194,7 +209,8 @@ See `config.toml` for all available options. Key settings: - `bind_host`: IP/interface to bind to (global) - `port`: Listen port (1965 is standard, per host override possible) - `max_concurrent_requests`: Connection limit (global) -- `log_level`: Logging verbosity (global, per host override possible) + +Logging is configured via the `RUST_LOG` environment variable (see Logging Configuration section). ## Certificate Management diff --git a/src/config.rs b/src/config.rs index 2a5271e..73f8db1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,7 +7,6 @@ pub struct Config { // Global defaults (optional) pub bind_host: Option, pub port: Option, - pub log_level: Option, pub max_concurrent_requests: Option, // Per-hostname configurations @@ -21,7 +20,7 @@ pub struct HostConfig { pub key: String, #[serde(default)] #[allow(dead_code)] - pub port: Option, // override global port + pub port: Option, // override global port #[serde(default)] #[allow(dead_code)] pub log_level: Option, // override global log level @@ -34,7 +33,6 @@ pub fn load_config(path: &str) -> Result> { // Extract global settings let bind_host = extract_string(&toml_value, "bind_host"); let port = extract_u16(&toml_value, "port"); - let log_level = extract_string(&toml_value, "log_level"); let max_concurrent_requests = extract_usize(&toml_value, "max_concurrent_requests"); // Extract host configurations @@ -43,7 +41,10 @@ pub fn load_config(path: &str) -> Result> { if let Some(table) = toml_value.as_table() { for (key, value) in table { // Skip global config keys - if matches!(key.as_str(), "bind_host" | "port" | "log_level" | "max_concurrent_requests") { + if matches!( + key.as_str(), + "bind_host" | "port" | "max_concurrent_requests" + ) { continue; } @@ -57,7 +58,11 @@ pub fn load_config(path: &str) -> Result> { // Validate hostname if !is_valid_hostname(key) { - return Err(format!("Invalid hostname '{}'. Hostnames must be valid DNS names.", key).into()); + return Err(format!( + "Invalid hostname '{}'. Hostnames must be valid DNS names.", + key + ) + .into()); } // Validate that root directory exists @@ -96,34 +101,53 @@ pub fn load_config(path: &str) -> Result> { Ok(Config { bind_host, port, - log_level, max_concurrent_requests, hosts, }) } fn extract_string(value: &Value, key: &str) -> Option { - value.get(key).and_then(|v| v.as_str()).map(|s| s.to_string()) + value + .get(key) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) } fn extract_string_from_table(table: &toml::map::Map, key: &str) -> Option { - table.get(key).and_then(|v| v.as_str()).map(|s| s.to_string()) + table + .get(key) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) } fn extract_u16(value: &Value, key: &str) -> Option { - value.get(key).and_then(|v| v.as_integer()).and_then(|i| u16::try_from(i).ok()) + value + .get(key) + .and_then(|v| v.as_integer()) + .and_then(|i| u16::try_from(i).ok()) } fn extract_u16_from_table(table: &toml::map::Map, key: &str) -> Option { - table.get(key).and_then(|v| v.as_integer()).and_then(|i| u16::try_from(i).ok()) + table + .get(key) + .and_then(|v| v.as_integer()) + .and_then(|i| u16::try_from(i).ok()) } fn extract_usize(value: &Value, key: &str) -> Option { - value.get(key).and_then(|v| v.as_integer()).and_then(|i| usize::try_from(i).ok()) + value + .get(key) + .and_then(|v| v.as_integer()) + .and_then(|i| usize::try_from(i).ok()) } -fn extract_required_string(table: &toml::map::Map, key: &str, section: &str) -> Result> { - table.get(key) +fn extract_required_string( + table: &toml::map::Map, + key: &str, + section: &str, +) -> Result> { + table + .get(key) .and_then(|v| v.as_str()) .map(|s| s.to_string()) .ok_or_else(|| format!("Missing required field '{}' in [{}] section", key, section).into()) @@ -204,7 +228,9 @@ mod tests { assert!(!is_valid_hostname("invalid..com")); assert!(!is_valid_hostname("invalid.com.")); assert!(!is_valid_hostname("inval!d.com")); - assert!(is_valid_hostname("too.long.label.that.exceeds.sixty.three.characters.example.com")); + assert!(is_valid_hostname( + "too.long.label.that.exceeds.sixty.three.characters.example.com" + )); } #[test] @@ -220,14 +246,19 @@ mod tests { fs::write(&cert_path, "dummy cert").unwrap(); fs::write(&key_path, "dummy key").unwrap(); - let content = format!(r#" + let content = format!( + r#" ["example.com"] root = "{}" cert = "{}" key = "{}" port = 1965 log_level = "info" - "#, root_dir.display(), cert_path.display(), key_path.display()); + "#, + root_dir.display(), + cert_path.display(), + key_path.display() + ); fs::write(&config_path, content).unwrap(); let config = load_config(config_path.to_str().unwrap()).unwrap(); @@ -262,7 +293,8 @@ mod tests { fs::write(&site2_cert, "dummy cert 2").unwrap(); fs::write(&site2_key, "dummy key 2").unwrap(); - let content = format!(r#" + let content = format!( + r#" ["site1.com"] root = "{}" cert = "{}" @@ -273,8 +305,14 @@ mod tests { cert = "{}" key = "{}" port = 1966 - "#, site1_root.display(), site1_cert.display(), site1_key.display(), - site2_root.display(), site2_cert.display(), site2_key.display()); + "#, + site1_root.display(), + site1_cert.display(), + site1_key.display(), + site2_root.display(), + site2_cert.display(), + site2_key.display() + ); fs::write(&config_path, content).unwrap(); let config = load_config(config_path.to_str().unwrap()).unwrap(); @@ -303,7 +341,10 @@ mod tests { let result = load_config(config_path.to_str().unwrap()); assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("No host configurations found")); + assert!(result + .unwrap_err() + .to_string() + .contains("No host configurations found")); } #[test] @@ -347,4 +388,4 @@ mod tests { // Config parsing will fail if required fields are missing assert!(load_config(config_path.to_str().unwrap()).is_err()); } -} \ No newline at end of file +} diff --git a/src/logging.rs b/src/logging.rs index 99a5c15..98c8cea 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -1,106 +1,5 @@ -use tokio::net::TcpStream; -use std::time::Instant; -use tokio_rustls::server::TlsStream; -use tracing_subscriber::fmt::format::Writer; -use tracing_subscriber::fmt::FormatFields; - -struct CleanLogFormatter; - -impl tracing_subscriber::fmt::FormatEvent for CleanLogFormatter -where - S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, - N: for<'a> tracing_subscriber::fmt::FormatFields<'a> + 'static, -{ - fn format_event( - &self, - ctx: &tracing_subscriber::fmt::FmtContext<'_, S, N>, - mut writer: Writer<'_>, - event: &tracing::Event<'_>, - ) -> std::fmt::Result { - // Write timestamp - let now = time::OffsetDateTime::now_utc(); - write!(writer, "{}-{:02}-{:02}T{:02}:{:02}:{:02} ", - now.year(), now.month() as u8, now.day(), - now.hour(), now.minute(), now.second())?; - - // Write level - let level = event.metadata().level(); - write!(writer, "{} ", level)?; - - // Write the message - ctx.format_fields(writer.by_ref(), event)?; - - writeln!(writer) - } -} - -#[allow(dead_code)] -pub struct RequestLogger { - request_url: String, - start_time: Instant, -} - -impl RequestLogger { - #[allow(dead_code)] - pub fn new(_stream: &TlsStream, request_url: String) -> Self { - Self { - request_url, - start_time: Instant::now(), - } - } - - - - #[allow(dead_code)] - pub fn log_error(self, status_code: u8, error_message: &str) { - let level = match status_code { - 41 | 51 => tracing::Level::WARN, - 59 => tracing::Level::ERROR, - _ => tracing::Level::ERROR, - }; - - let request_path = self.request_url.strip_prefix("gemini://localhost").unwrap_or(&self.request_url); - - match level { - tracing::Level::WARN => tracing::warn!("unknown \"{}\" {} \"{}\"", request_path, status_code, error_message), - tracing::Level::ERROR => tracing::error!("unknown \"{}\" {} \"{}\"", request_path, status_code, error_message), - _ => {} - } - } - - -} - -#[allow(dead_code)] -fn extract_client_ip(stream: &TlsStream) -> String { - let (tcp_stream, _) = stream.get_ref(); - match tcp_stream.peer_addr() { - Ok(addr) => addr.to_string(), - Err(_) => "unknown".to_string(), - } -} - -pub fn init_logging(level: &str) { - use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; - - let level = match level.to_lowercase().as_str() { - "error" => tracing::Level::ERROR, - "warn" => tracing::Level::WARN, - "info" => tracing::Level::INFO, - "debug" => tracing::Level::DEBUG, - "trace" => tracing::Level::TRACE, - _ => { - eprintln!("Warning: Invalid log level '{}', defaulting to 'info'", level); - tracing::Level::INFO - } - }; - - tracing_subscriber::registry() - .with(tracing_subscriber::fmt::layer() - .event_format(CleanLogFormatter)) - .with(tracing_subscriber::filter::LevelFilter::from_level(level)) - .init(); -} +// Logging module - now unused as logging is handled directly in main.rs +// All logging functionality moved to main.rs with RUST_LOG environment variable support #[cfg(test)] mod tests { @@ -109,4 +8,4 @@ mod tests { // Basic test to ensure logging module compiles assert!(true); } -} \ No newline at end of file +} diff --git a/src/main.rs b/src/main.rs index a92a41f..90b26bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,8 @@ mod config; -mod tls; +mod logging; mod request; mod server; -mod logging; +mod tls; use clap::Parser; use rustls::ServerConfig; @@ -10,9 +10,11 @@ use std::path::Path; use std::sync::Arc; use tokio::net::TcpListener; use tokio_rustls::TlsAcceptor; -use logging::init_logging; +use tracing_subscriber::EnvFilter; -fn create_tls_config(hosts: &std::collections::HashMap) -> Result, Box> { +fn create_tls_config( + hosts: &std::collections::HashMap, +) -> Result, Box> { // For Phase 3, we'll use the first host's certificate for all connections // TODO: Phase 4 could implement proper SNI-based certificate selection let first_host = hosts.values().next().ok_or("No hosts configured")?; @@ -28,7 +30,10 @@ fn create_tls_config(hosts: &std::collections::HashMap) { +fn print_startup_info( + config: &config::Config, + hosts: &std::collections::HashMap, +) { println!("Pollux Gemini Server (Virtual Host Mode)"); println!("Configured hosts:"); for (hostname, host_config) in hosts { @@ -41,17 +46,13 @@ fn print_startup_info(config: &config::Config, hosts: &std::collections::HashMap if let Some(port) = config.port { println!(" Default port: {}", port); } - if let Some(ref level) = config.log_level { - println!(" Log level: {}", level); - } + if let Some(max_concurrent) = config.max_concurrent_requests { println!(" Max concurrent requests: {}", max_concurrent); } println!(); // Add spacing before connections start } - - #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { @@ -64,8 +65,6 @@ struct Args { test_processing_delay: Option, } - - #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); @@ -73,21 +72,41 @@ async fn main() -> Result<(), Box> { // Load config let config_path = args.config.as_deref().unwrap_or("/etc/pollux/config.toml"); if !std::path::Path::new(&config_path).exists() { - eprintln!("Error: Config file '{}' not found", config_path); - eprintln!("Create the config file with virtual host sections like:"); - eprintln!("[example.com]"); - eprintln!("root = \"/srv/gemini/example.com/gemini/\""); - eprintln!("cert = \"/srv/gemini/example.com/tls/fullchain.pem\""); - eprintln!("key = \"/srv/gemini/example.com/tls/privkey.pem\""); + // User guidance goes to stdout BEFORE initializing tracing + // Use direct stderr for error, stdout for guidance + use std::io::Write; + let mut stderr = std::io::stderr(); + let mut stdout = std::io::stdout(); + + writeln!(stderr, "Config file '{}' not found", config_path).unwrap(); + writeln!( + stdout, + "Create the config file with virtual host sections like:" + ) + .unwrap(); + writeln!(stdout, "[example.com]").unwrap(); + writeln!(stdout, "root = \"/var/gemini\"").unwrap(); + writeln!(stdout, "cert = \"/etc/pollux/tls/cert.pem\"").unwrap(); + writeln!(stdout, "key = \"/etc/pollux/tls/key.pem\"").unwrap(); + + stdout.flush().unwrap(); std::process::exit(1); } + // Initialize logging with RUST_LOG support AFTER basic config checks + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_writer(std::io::stderr) + .init(); + // Load and parse config let config = match config::load_config(config_path) { Ok(config) => config, Err(e) => { - eprintln!("Error: Failed to parse config file '{}': {}", config_path, e); - eprintln!("Check the TOML syntax and ensure host sections are properly formatted."); + tracing::error!("Failed to parse config file '{}': {}", config_path, e); + tracing::error!( + "Check the TOML syntax and ensure host sections are properly formatted." + ); std::process::exit(1); } }; @@ -97,61 +116,88 @@ async fn main() -> Result<(), Box> { // Validate root directory exists and is readable let root_path = Path::new(&host_config.root); if !root_path.exists() { - eprintln!("Error: Root directory '{}' for host '{}' does not exist", host_config.root, hostname); - eprintln!("Create the directory and add your Gemini files (.gmi, .txt, images)"); + tracing::error!( + "Root directory '{}' for host '{}' does not exist", + host_config.root, + hostname + ); + tracing::error!("Create the directory and add your Gemini files (.gmi, .txt, images)"); std::process::exit(1); } if !root_path.is_dir() { - eprintln!("Error: Root path '{}' for host '{}' is not a directory", host_config.root, hostname); - eprintln!("The 'root' field must point to a directory containing your content"); + tracing::error!( + "Root path '{}' for host '{}' is not a directory", + host_config.root, + hostname + ); + tracing::error!("The 'root' field must point to a directory containing your content"); std::process::exit(1); } if let Err(e) = std::fs::read_dir(root_path) { - eprintln!("Error: Cannot read root directory '{}' for host '{}': {}", host_config.root, hostname, e); - eprintln!("Ensure the directory exists and the server user has read permission"); + tracing::error!( + "Cannot read root directory '{}' for host '{}': {}", + host_config.root, + hostname, + e + ); + tracing::error!("Ensure the directory exists and the server user has read permission"); std::process::exit(1); } // Validate certificate files (always required for TLS) let cert_path = Path::new(&host_config.cert); if !cert_path.exists() { - eprintln!("Error: Certificate file '{}' for host '{}' does not exist", host_config.cert, hostname); + eprintln!( + "Error: Certificate file '{}' for host '{}' does not exist", + host_config.cert, hostname + ); eprintln!("Generate or obtain TLS certificates for your domain"); std::process::exit(1); } if let Err(e) = std::fs::File::open(cert_path) { - eprintln!("Error: Cannot read certificate file '{}' for host '{}': {}", host_config.cert, hostname, e); - eprintln!("Ensure the file exists and the server user has read permission"); + tracing::error!( + "Cannot read certificate file '{}' for host '{}': {}", + host_config.cert, + hostname, + e + ); + tracing::error!("Ensure the file exists and the server user has read permission"); std::process::exit(1); } let key_path = Path::new(&host_config.key); if !key_path.exists() { - eprintln!("Error: Private key file '{}' for host '{}' does not exist", host_config.key, hostname); - eprintln!("Generate or obtain TLS private key for your domain"); + tracing::error!( + "Private key file '{}' for host '{}' does not exist", + host_config.key, + hostname + ); + tracing::error!("Generate or obtain TLS private key for your domain"); std::process::exit(1); } if let Err(e) = std::fs::File::open(key_path) { - eprintln!("Error: Cannot read private key file '{}' for host '{}': {}", host_config.key, hostname, e); - eprintln!("Ensure the file exists and the server user has read permission"); + tracing::error!( + "Cannot read private key file '{}' for host '{}': {}", + host_config.key, + hostname, + e + ); + tracing::error!("Ensure the file exists and the server user has read permission"); std::process::exit(1); } } - // Initialize logging - let log_level = config.log_level.as_deref().unwrap_or("info"); - init_logging(log_level); - // Validate max concurrent requests let max_concurrent_requests = config.max_concurrent_requests.unwrap_or(1000); if max_concurrent_requests == 0 || max_concurrent_requests > 1_000_000 { - eprintln!("Error: max_concurrent_requests must be between 1 and 1,000,000"); + tracing::error!("max_concurrent_requests must be between 1 and 1,000,000"); std::process::exit(1); } // TESTING ONLY: Read delay argument (debug builds only) #[cfg(debug_assertions)] - let test_processing_delay = args.test_processing_delay + let test_processing_delay = args + .test_processing_delay .filter(|&d| d > 0 && d <= 300) .unwrap_or(0); @@ -172,7 +218,10 @@ async fn main() -> Result<(), Box> { let port = config.port.unwrap_or(1965); let listener = TcpListener::bind(format!("{}:{}", bind_host, port)).await?; - println!("Listening on {}:{} for all virtual hosts (TLS enabled)", bind_host, port); + println!( + "Listening on {}:{} for all virtual hosts (TLS enabled)", + bind_host, port + ); loop { let (stream, _) = listener.accept().await?; @@ -186,14 +235,21 @@ async fn main() -> Result<(), Box> { // TLS connection with hostname routing match acceptor_clone.accept(stream).await { Ok(tls_stream) => { - if let Err(e) = server::handle_connection(tls_stream, &hosts_clone, max_concurrent, test_delay).await { - eprintln!("Error handling connection: {}", e); + if let Err(e) = server::handle_connection( + tls_stream, + &hosts_clone, + max_concurrent, + test_delay, + ) + .await + { + tracing::error!("Error handling connection: {}", e); } } Err(e) => { - eprintln!("TLS handshake failed: {}", e); + tracing::error!("TLS handshake failed: {}", e); } } }); } -} \ No newline at end of file +} diff --git a/src/request.rs b/src/request.rs index b0b95c4..3fc591c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -11,7 +11,7 @@ pub fn parse_gemini_url(request: &str, hostname: &str, expected_port: u16) -> Re 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]; @@ -20,21 +20,23 @@ pub fn parse_gemini_url(request: &str, hostname: &str, expected_port: u16) -> Re } else { (host_port, None) }; - + // Validate host if host != hostname { - return Err(()); // Hostname mismatch + return Err(()); // Hostname mismatch } - + // Validate port - let port = port_str - .and_then(|p| p.parse::().ok()) - .unwrap_or(1965); + let port = port_str.and_then(|p| p.parse::().ok()).unwrap_or(1965); if port != expected_port { - return Err(()); // Port mismatch + return Err(()); // Port mismatch } - - let path = if host_port_end < url.len() { &url[host_port_end..] } else { "/" }; + + let path = if host_port_end < url.len() { + &url[host_port_end..] + } else { + "/" + }; Ok(path.trim().to_string()) } else { Err(()) @@ -58,11 +60,11 @@ pub fn resolve_file_path(path: &str, dir: &str) -> Result { // Path validation failed - treat as not found Err(PathResolutionError::NotFound) - }, + } } } @@ -90,8 +92,18 @@ mod tests { #[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())); + 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] @@ -130,14 +142,20 @@ mod tests { #[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)); + 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)); + assert_eq!( + resolve_file_path("/nonexistent.gmi", temp_dir.path().to_str().unwrap()), + Err(PathResolutionError::NotFound) + ); } #[test] @@ -163,4 +181,4 @@ mod tests { let path = Path::new("test"); assert_eq!(get_mime_type(path), "application/octet-stream"); } -} \ No newline at end of file +} diff --git a/src/server.rs b/src/server.rs index 325e047..3a06e83 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,4 +1,4 @@ -use crate::request::{resolve_file_path, get_mime_type}; +use crate::request::{get_mime_type, resolve_file_path}; use std::fs; use std::io; use std::path::Path; @@ -8,8 +8,6 @@ use tokio::net::TcpStream; use tokio::time::{timeout, Duration}; use tokio_rustls::server::TlsStream; - - static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0); /// Extract hostname and path from a Gemini URL @@ -35,8 +33,7 @@ pub fn extract_hostname_and_path(request: &str) -> Result<(String, String), ()> } // URL decode the path - let decoded_path = urlencoding::decode(&path) - .map_err(|_| ())?; + let decoded_path = urlencoding::decode(&path).map_err(|_| ())?; Ok((hostname.to_string(), decoded_path.to_string())) } @@ -54,7 +51,10 @@ pub async fn handle_connection( let read_future = async { loop { if request_buf.len() >= MAX_REQUEST_SIZE { - return Err(tokio::io::Error::new(tokio::io::ErrorKind::InvalidData, "Request too large")); + return Err(tokio::io::Error::new( + tokio::io::ErrorKind::InvalidData, + "Request too large", + )); } let mut byte = [0; 1]; stream.read_exact(&mut byte).await?; @@ -154,11 +154,7 @@ pub async fn handle_connection( Ok(()) } -async fn serve_file( - stream: &mut S, - file_path: &Path, - _request: &str, -) -> io::Result<()> +async fn serve_file(stream: &mut S, file_path: &Path, _request: &str) -> io::Result<()> where S: AsyncWriteExt + Unpin, { @@ -177,14 +173,11 @@ where Ok(()) } -async fn send_response( - stream: &mut S, - response: &str, -) -> io::Result<()> +async fn send_response(stream: &mut S, response: &str) -> io::Result<()> where S: AsyncWriteExt + Unpin, { stream.write_all(response.as_bytes()).await?; stream.flush().await?; Ok(()) -} \ No newline at end of file +} diff --git a/src/tls.rs b/src/tls.rs index ff741b7..04b4cff 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -24,5 +24,8 @@ pub fn load_private_key(filename: &str) -> io::Result { } } - Err(io::Error::new(io::ErrorKind::InvalidData, "No supported private key found")) -} \ No newline at end of file + Err(io::Error::new( + io::ErrorKind::InvalidData, + "No supported private key found", + )) +} diff --git a/tests/common.rs b/tests/common.rs index ac0d11c..7f401cd 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -7,8 +7,22 @@ pub fn generate_test_certificates_for_host(temp_dir: &Path, hostname: &str) { // Generate self-signed certificate for testing // This is a simplified version - in production, use proper certificates - std::fs::write(&cert_path, format!("-----BEGIN CERTIFICATE-----\nTest cert for {}\n-----END CERTIFICATE-----\n", hostname)).unwrap(); - std::fs::write(&key_path, format!("-----BEGIN PRIVATE KEY-----\nTest key for {}\n-----END PRIVATE KEY-----\n", hostname)).unwrap(); + std::fs::write( + &cert_path, + format!( + "-----BEGIN CERTIFICATE-----\nTest cert for {}\n-----END CERTIFICATE-----\n", + hostname + ), + ) + .unwrap(); + std::fs::write( + &key_path, + format!( + "-----BEGIN PRIVATE KEY-----\nTest key for {}\n-----END PRIVATE KEY-----\n", + hostname + ), + ) + .unwrap(); } use tempfile::TempDir; @@ -42,12 +56,19 @@ fn generate_test_certificates(temp_dir: &Path) { // Use openssl to generate a test certificate let output = Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key_path.to_string_lossy(), - "-out", &cert_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key_path.to_string_lossy(), + "-out", + &cert_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=localhost" + "-subj", + "/CN=localhost", ]) .output(); @@ -60,8 +81,3 @@ fn generate_test_certificates(temp_dir: &Path) { } } } - - - - - diff --git a/tests/config_validation.rs b/tests/config_validation.rs index dc7f934..0c71c27 100644 --- a/tests/config_validation.rs +++ b/tests/config_validation.rs @@ -7,87 +7,80 @@ fn test_missing_config_file() { let output = Command::new(env!("CARGO_BIN_EXE_pollux")) .arg("--config") .arg("nonexistent.toml") + .env("RUST_LOG", "error") .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); + let stdout = String::from_utf8(output.stdout).unwrap(); assert!(stderr.contains("Config file 'nonexistent.toml' not found")); - assert!(stderr.contains("Create the config file with") || stderr.contains("Add at least one")); -} - -#[test] -fn test_no_host_sections() { - let temp_dir = common::setup_test_environment(); - let config_path = temp_dir.path().join("config.toml"); - let config_content = r#" -bind_host = "127.0.0.1" -port = 1965 -# No host sections defined -"#; - std::fs::write(&config_path, config_content).unwrap(); - - let output = Command::new(env!("CARGO_BIN_EXE_pollux")) - .arg("--config") - .arg(&config_path) - .output() - .unwrap(); - - assert!(!output.status.success()); - let stderr = String::from_utf8(output.stderr).unwrap(); - assert!(stderr.contains("No host configurations found")); - assert!(stderr.contains("Add at least one [hostname] section")); + assert!(stdout.contains("Create the config file with")); } #[test] fn test_nonexistent_root_directory() { let temp_dir = common::setup_test_environment(); let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" ["example.com"] root = "/definitely/does/not/exist" cert = "{}" key = "{}" - "#, temp_dir.path().join("cert.pem").display(), temp_dir.path().join("key.pem").display()); + "#, + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_pollux")) .arg("--config") .arg(&config_path) + .env("RUST_LOG", "error") .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); - assert!(stderr.contains("Error for host 'example.com': Root directory '/definitely/does/not/exist' does not exist")); - assert!(stderr.contains("Create the directory and add your Gemini files (.gmi, .txt, images)")); + assert!(stderr.contains("Failed to parse config file")); + assert!(stderr.contains( + "Error for host 'example.com': Root directory '/definitely/does/not/exist' does not exist" + )); } #[test] fn test_missing_certificate_file() { let temp_dir = common::setup_test_environment(); let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" ["example.com"] root = "{}" cert = "/nonexistent/cert.pem" key = "{}" - "#, temp_dir.path().join("content").display(), temp_dir.path().join("key.pem").display()); + "#, + temp_dir.path().join("content").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_pollux")) .arg("--config") .arg(&config_path) + .env("RUST_LOG", "error") .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); - assert!(stderr.contains("Error for host 'example.com': Certificate file '/nonexistent/cert.pem' does not exist")); + assert!(stderr.contains( + "Error for host 'example.com': Certificate file '/nonexistent/cert.pem' does not exist" + )); assert!(stderr.contains("Generate or obtain TLS certificates for your domain")); } @@ -96,7 +89,8 @@ fn test_valid_config_startup() { let temp_dir = common::setup_test_environment(); let port = 1967 + (std::process::id() % 1000) as u16; let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -104,7 +98,12 @@ port = {} root = "{}" cert = "{}" key = "{}" -"#, port, temp_dir.path().join("content").display(), temp_dir.path().join("cert.pem").display(), temp_dir.path().join("key.pem").display()); +"#, + port, + temp_dir.path().join("content").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); let mut server_process = Command::new(env!("CARGO_BIN_EXE_pollux")) @@ -117,7 +116,10 @@ key = "{}" std::thread::sleep(std::time::Duration::from_millis(500)); // Check server is still running (didn't exit with error) - assert!(server_process.try_wait().unwrap().is_none(), "Server should still be running with valid config"); + assert!( + server_process.try_wait().unwrap().is_none(), + "Server should still be running with valid config" + ); // Kill server server_process.kill().unwrap(); @@ -141,24 +143,38 @@ fn test_valid_multiple_hosts_startup() { // Generate certificate for host1 let cert_result1 = std::process::Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key1_path.to_string_lossy(), - "-out", &cert1_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key1_path.to_string_lossy(), + "-out", + &cert1_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=host1.com" + "-subj", + "/CN=host1.com", ]) .output(); // Generate certificate for host2 let cert_result2 = std::process::Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key2_path.to_string_lossy(), - "-out", &cert2_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key2_path.to_string_lossy(), + "-out", + &cert2_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=host2.com" + "-subj", + "/CN=host2.com", ]) .output(); @@ -167,7 +183,8 @@ fn test_valid_multiple_hosts_startup() { } let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -181,13 +198,14 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - temp_dir.path().join("host1").display(), - cert1_path.display(), - key1_path.display(), - temp_dir.path().join("host2").display(), - cert2_path.display(), - key2_path.display()); + port, + temp_dir.path().join("host1").display(), + cert1_path.display(), + key1_path.display(), + temp_dir.path().join("host2").display(), + cert2_path.display(), + key2_path.display() + ); std::fs::write(&config_path, config_content).unwrap(); @@ -201,7 +219,10 @@ key = "{}" std::thread::sleep(std::time::Duration::from_millis(500)); // Check server is still running (didn't exit with error) - assert!(server_process.try_wait().unwrap().is_none(), "Server should start with valid multiple host config"); + assert!( + server_process.try_wait().unwrap().is_none(), + "Server should start with valid multiple host config" + ); // Kill server server_process.kill().unwrap(); @@ -222,12 +243,19 @@ fn test_multiple_hosts_missing_certificate() { let cert_result = std::process::Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key1_path.to_string_lossy(), - "-out", &cert1_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key1_path.to_string_lossy(), + "-out", + &cert1_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=host1.com" + "-subj", + "/CN=host1.com", ]) .output(); @@ -235,7 +263,8 @@ fn test_multiple_hosts_missing_certificate() { panic!("Failed to generate test certificate"); } - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" ["host1.com"] @@ -248,22 +277,26 @@ root = "{}" cert = "/nonexistent/cert.pem" key = "/nonexistent/key.pem" "#, - temp_dir.path().join("host1").display(), - cert1_path.display(), - key1_path.display(), - temp_dir.path().join("host2").display()); + temp_dir.path().join("host1").display(), + cert1_path.display(), + key1_path.display(), + temp_dir.path().join("host2").display() + ); std::fs::write(&config_path, config_content).unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_pollux")) .arg("--config") .arg(&config_path) + .env("RUST_LOG", "error") .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); - assert!(stderr.contains("Error for host 'host2.com': Certificate file '/nonexistent/cert.pem' does not exist")); + assert!(stderr.contains( + "Error for host 'host2.com': Certificate file '/nonexistent/cert.pem' does not exist" + )); } #[test] @@ -284,24 +317,38 @@ fn test_multiple_hosts_invalid_hostname() { // Generate certificate for valid host let cert_result1 = std::process::Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key1_path.to_string_lossy(), - "-out", &cert1_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key1_path.to_string_lossy(), + "-out", + &cert1_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=valid.com" + "-subj", + "/CN=valid.com", ]) .output(); // Generate certificate for invalid host (hostname validation happens before cert validation) let cert_result2 = std::process::Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key2_path.to_string_lossy(), - "-out", &cert2_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key2_path.to_string_lossy(), + "-out", + &cert2_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=invalid.com" + "-subj", + "/CN=invalid.com", ]) .output(); @@ -309,7 +356,8 @@ fn test_multiple_hosts_invalid_hostname() { panic!("Failed to generate test certificates"); } - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" ["valid.com"] @@ -322,22 +370,24 @@ root = "{}" cert = "{}" key = "{}" "#, - temp_dir.path().join("validhost").display(), - cert1_path.display(), - key1_path.display(), - temp_dir.path().join("invalidhost").display(), - cert2_path.display(), - key2_path.display()); + temp_dir.path().join("validhost").display(), + cert1_path.display(), + key1_path.display(), + temp_dir.path().join("invalidhost").display(), + cert2_path.display(), + key2_path.display() + ); std::fs::write(&config_path, config_content).unwrap(); let output = Command::new(env!("CARGO_BIN_EXE_pollux")) .arg("--config") .arg(&config_path) + .env("RUST_LOG", "error") .output() .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.contains("Invalid hostname 'bad..host.com'. Hostnames must be valid DNS names.")); -} \ No newline at end of file +} diff --git a/tests/rate_limiting.rs b/tests/rate_limiting.rs index b02459c..fa7c4c1 100644 --- a/tests/rate_limiting.rs +++ b/tests/rate_limiting.rs @@ -13,7 +13,8 @@ fn test_rate_limiting_with_concurrent_requests() { let cert_path = temp_dir.path().join("cert.pem"); let key_path = temp_dir.path().join("key.pem"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} max_concurrent_requests = 1 @@ -22,7 +23,12 @@ max_concurrent_requests = 1 root = "{}" cert = "{}" key = "{}" - "#, port, root_dir.display(), cert_path.display(), key_path.display()); + "#, + port, + root_dir.display(), + cert_path.display(), + key_path.display() + ); std::fs::write(&config_path, config_content).unwrap(); // Start server binary with test delay to simulate processing time @@ -30,7 +36,7 @@ key = "{}" .arg("--config") .arg(&config_path) .arg("--test-processing-delay") - .arg("3") // 3 second delay per request + .arg("3") // 3 second delay per request .spawn() .expect("Failed to start server"); @@ -68,13 +74,30 @@ key = "{}" let rate_limited_count = results.iter().filter(|r| r.starts_with("41")).count(); // Debug output - println!("Results: {:?}", results); - println!("Success: {}, Rate limited: {}", success_count, rate_limited_count); + tracing::debug!("Test results: {:?}", results); + tracing::debug!( + "Success: {}, Rate limited: {}", + success_count, + rate_limited_count + ); // Strict validation - rate limiting must work deterministically with delay - assert_eq!(success_count, 1, "Expected exactly 1 successful request with limit=1, got {}. Results: {:?}", success_count, results); - assert_eq!(rate_limited_count, 4, "Expected exactly 4 rate limited requests with limit=1, got {}. Results: {:?}", rate_limited_count, results); + assert_eq!( + success_count, 1, + "Expected exactly 1 successful request with limit=1, got {}. Results: {:?}", + success_count, results + ); + assert_eq!( + rate_limited_count, 4, + "Expected exactly 4 rate limited requests with limit=1, got {}. Results: {:?}", + rate_limited_count, results + ); // Verify all requests received valid responses - assert_eq!(success_count + rate_limited_count, 5, "All 5 requests should receive responses. Results: {:?}", results); -} \ No newline at end of file + assert_eq!( + success_count + rate_limited_count, + 5, + "All 5 requests should receive responses. Results: {:?}", + results + ); +} diff --git a/tests/virtual_host_config.rs b/tests/virtual_host_config.rs index ad64d31..e568097 100644 --- a/tests/virtual_host_config.rs +++ b/tests/virtual_host_config.rs @@ -17,12 +17,19 @@ fn test_single_host_config() { let cert_result = Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &key_path.to_string_lossy(), - "-out", &cert_path.to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &key_path.to_string_lossy(), + "-out", + &cert_path.to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=example.com" + "-subj", + "/CN=example.com", ]) .output(); @@ -30,7 +37,8 @@ fn test_single_host_config() { panic!("Failed to generate test certificates for config test"); } - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -38,7 +46,12 @@ port = {} root = "{}" cert = "{}" key = "{}" -"#, port, content_dir.display(), cert_path.display(), key_path.display()); +"#, + port, + content_dir.display(), + cert_path.display(), + key_path.display() + ); std::fs::write(&config_path, config_content).unwrap(); let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux")) @@ -48,7 +61,10 @@ key = "{}" .unwrap(); std::thread::sleep(std::time::Duration::from_millis(500)); - assert!(server_process.try_wait().unwrap().is_none(), "Server should start with valid single host config"); + assert!( + server_process.try_wait().unwrap().is_none(), + "Server should start with valid single host config" + ); server_process.kill().unwrap(); } @@ -56,7 +72,8 @@ key = "{}" fn test_multiple_hosts_config() { let temp_dir = common::setup_test_environment(); let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" [site1.com] root = "{}" cert = "{}" @@ -69,12 +86,14 @@ key = "{}" bind_host = "127.0.0.1" port = 1965 -"#, temp_dir.path().join("site1").display(), - temp_dir.path().join("site1_cert.pem").display(), - temp_dir.path().join("site1_key.pem").display(), - temp_dir.path().join("site2").display(), - temp_dir.path().join("site2_cert.pem").display(), - temp_dir.path().join("site2_key.pem").display()); +"#, + temp_dir.path().join("site1").display(), + temp_dir.path().join("site1_cert.pem").display(), + temp_dir.path().join("site1_key.pem").display(), + temp_dir.path().join("site2").display(), + temp_dir.path().join("site2_cert.pem").display(), + temp_dir.path().join("site2_key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); // Create additional directories and generate certificates @@ -87,24 +106,38 @@ port = 1965 // Site 1 certificate let cert_result1 = Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &temp_dir.path().join("site1_key.pem").to_string_lossy(), - "-out", &temp_dir.path().join("site1_cert.pem").to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &temp_dir.path().join("site1_key.pem").to_string_lossy(), + "-out", + &temp_dir.path().join("site1_cert.pem").to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=site1.com" + "-subj", + "/CN=site1.com", ]) .output(); // Site 2 certificate let cert_result2 = Command::new("openssl") .args(&[ - "req", "-x509", "-newkey", "rsa:2048", - "-keyout", &temp_dir.path().join("site2_key.pem").to_string_lossy(), - "-out", &temp_dir.path().join("site2_cert.pem").to_string_lossy(), - "-days", "1", + "req", + "-x509", + "-newkey", + "rsa:2048", + "-keyout", + &temp_dir.path().join("site2_key.pem").to_string_lossy(), + "-out", + &temp_dir.path().join("site2_cert.pem").to_string_lossy(), + "-days", + "1", "-nodes", - "-subj", "/CN=site2.org" + "-subj", + "/CN=site2.org", ]) .output(); @@ -114,7 +147,8 @@ port = 1965 // Test server starts successfully with multiple host config let port = 1968 + (std::process::id() % 1000) as u16; - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -128,13 +162,14 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - temp_dir.path().join("site1").display(), - temp_dir.path().join("site1_cert.pem").display(), - temp_dir.path().join("site1_key.pem").display(), - temp_dir.path().join("site2").display(), - temp_dir.path().join("site2_cert.pem").display(), - temp_dir.path().join("site2_key.pem").display()); + port, + temp_dir.path().join("site1").display(), + temp_dir.path().join("site1_cert.pem").display(), + temp_dir.path().join("site1_key.pem").display(), + temp_dir.path().join("site2").display(), + temp_dir.path().join("site2_cert.pem").display(), + temp_dir.path().join("site2_key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux")) @@ -144,7 +179,10 @@ key = "{}" .unwrap(); std::thread::sleep(std::time::Duration::from_millis(500)); - assert!(server_process.try_wait().unwrap().is_none(), "Server should start with valid multiple host config"); + assert!( + server_process.try_wait().unwrap().is_none(), + "Server should start with valid multiple host config" + ); server_process.kill().unwrap(); } @@ -177,14 +215,17 @@ root = "/tmp/content" fn test_invalid_hostname_config() { let temp_dir = common::setup_test_environment(); let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" ["invalid"] root = "{}" cert = "{}" key = "{}" -"#, temp_dir.path().join("content").display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); +"#, + temp_dir.path().join("content").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); let output = std::process::Command::new(env!("CARGO_BIN_EXE_pollux")) @@ -224,7 +265,8 @@ port = 1965 fn test_duplicate_hostname_config() { let temp_dir = common::setup_test_environment(); let config_path = temp_dir.path().join("config.toml"); - let config_content = format!(r#" + let config_content = format!( + r#" [example.com] root = "{}" cert = "{}" @@ -234,12 +276,14 @@ key = "{}" root = "{}" cert = "{}" key = "{}" -"#, temp_dir.path().join("path1").display(), - temp_dir.path().join("cert1.pem").display(), - temp_dir.path().join("key1.pem").display(), - temp_dir.path().join("path2").display(), - temp_dir.path().join("cert2.pem").display(), - temp_dir.path().join("key2.pem").display()); +"#, + temp_dir.path().join("path1").display(), + temp_dir.path().join("cert1.pem").display(), + temp_dir.path().join("key1.pem").display(), + temp_dir.path().join("path2").display(), + temp_dir.path().join("cert2.pem").display(), + temp_dir.path().join("key2.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); // Create the directories and certs @@ -266,7 +310,8 @@ fn test_host_with_port_override() { let config_path = temp_dir.path().join("config.toml"); // Test server starts successfully let port = 1969 + (std::process::id() % 1000) as u16; - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -275,10 +320,12 @@ root = "{}" cert = "{}" key = "{}" port = 1970 # Override global port -"#, port, - temp_dir.path().join("content").display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); +"#, + port, + temp_dir.path().join("content").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux")) @@ -288,7 +335,10 @@ port = 1970 # Override global port .unwrap(); std::thread::sleep(std::time::Duration::from_millis(500)); - assert!(server_process.try_wait().unwrap().is_none(), "Server should start with host port override"); + assert!( + server_process.try_wait().unwrap().is_none(), + "Server should start with host port override" + ); server_process.kill().unwrap(); } @@ -303,4 +353,4 @@ fn test_config_file_not_found() { assert!(!output.status.success()); let stderr = String::from_utf8(output.stderr).unwrap(); assert!(stderr.contains("Config file 'nonexistent.toml' not found")); -} \ No newline at end of file +} diff --git a/tests/virtual_host_integration.rs b/tests/virtual_host_integration.rs index b76462b..57bffee 100644 --- a/tests/virtual_host_integration.rs +++ b/tests/virtual_host_integration.rs @@ -15,10 +15,7 @@ fn test_concurrent_requests_multiple_hosts() { for host in &hosts { let root_dir = temp_dir.path().join(host.replace(".", "_")); std::fs::create_dir(&root_dir).unwrap(); - std::fs::write( - root_dir.join("index.gmi"), - format!("Welcome to {}", host), - ).unwrap(); + std::fs::write(root_dir.join("index.gmi"), format!("Welcome to {}", host)).unwrap(); host_roots.push(root_dir); } @@ -26,23 +23,28 @@ fn test_concurrent_requests_multiple_hosts() { let config_path = temp_dir.path().join("config.toml"); let port = 1969 + (std::process::id() % 1000) as u16; - let mut config_content = format!(r#" + let mut config_content = format!( + r#" bind_host = "127.0.0.1" port = {} -"#, port); +"#, + port + ); for (i, host) in hosts.iter().enumerate() { - config_content.push_str(&format!(r#" + config_content.push_str(&format!( + r#" ["{}"] root = "{}" cert = "{}" key = "{}" "#, - host, - host_roots[i].display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display())); + host, + host_roots[i].display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + )); } std::fs::write(&config_path, config_content).unwrap(); @@ -65,9 +67,20 @@ key = "{}" let port_clone = Arc::clone(&port_arc); let handle = thread::spawn(move || { - let response = make_gemini_request("127.0.0.1", *port_clone, &format!("gemini://{}/", host)); - assert!(response.starts_with("20"), "Request {} failed: {}", i, response); - assert!(response.contains(&format!("Welcome to {}", host)), "Wrong content for request {}: {}", i, response); + let response = + make_gemini_request("127.0.0.1", *port_clone, &format!("gemini://{}/", host)); + assert!( + response.starts_with("20"), + "Request {} failed: {}", + i, + response + ); + assert!( + response.contains(&format!("Welcome to {}", host)), + "Wrong content for request {}: {}", + i, + response + ); response }); @@ -97,7 +110,8 @@ fn test_mixed_valid_invalid_hostnames() { // Create config let config_path = temp_dir.path().join("config.toml"); let port = 1970 + (std::process::id() % 1000) as u16; - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -106,10 +120,11 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - root_dir.display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); + port, + root_dir.display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); // Start server with TLS @@ -123,8 +138,16 @@ key = "{}" // Test valid hostname let valid_response = make_gemini_request("127.0.0.1", port, "gemini://valid.com/"); - assert!(valid_response.starts_with("20"), "Valid host should work: {}", valid_response); - assert!(valid_response.contains("Valid site content"), "Should serve correct content: {}", valid_response); + assert!( + valid_response.starts_with("20"), + "Valid host should work: {}", + valid_response + ); + assert!( + valid_response.contains("Valid site content"), + "Should serve correct content: {}", + valid_response + ); // Test various invalid hostnames let invalid_hosts = vec![ @@ -135,8 +158,14 @@ key = "{}" ]; for invalid_host in invalid_hosts { - let response = make_gemini_request("127.0.0.1", port, &format!("gemini://{}/", invalid_host)); - assert!(response.starts_with("53"), "Invalid host '{}' should return 53, got: {}", invalid_host, response); + let response = + make_gemini_request("127.0.0.1", port, &format!("gemini://{}/", invalid_host)); + assert!( + response.starts_with("53"), + "Invalid host '{}' should return 53, got: {}", + invalid_host, + response + ); } server_process.kill().unwrap(); @@ -153,7 +182,8 @@ fn test_load_performance_basic() { let config_path = temp_dir.path().join("config.toml"); let port = 1971 + (std::process::id() % 1000) as u16; - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -162,10 +192,11 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - root_dir.display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); + port, + root_dir.display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); // Start server with TLS @@ -183,17 +214,30 @@ key = "{}" for i in 0..NUM_REQUESTS { let response = make_gemini_request("127.0.0.1", port, "gemini://perf.com/"); - assert!(response.starts_with("20"), "Request {} failed: {}", i, response); + assert!( + response.starts_with("20"), + "Request {} failed: {}", + i, + response + ); } let elapsed = start.elapsed(); let avg_time = elapsed.as_millis() as f64 / NUM_REQUESTS as f64; - println!("Processed {} requests in {:?} (avg: {:.2}ms per request)", - NUM_REQUESTS, elapsed, avg_time); + tracing::debug!( + "Processed {} requests in {:?} (avg: {:.2}ms per request)", + NUM_REQUESTS, + elapsed, + avg_time + ); // Basic performance check - should be reasonably fast - assert!(avg_time < 100.0, "Average request time too slow: {:.2}ms", avg_time); + assert!( + avg_time < 100.0, + "Average request time too slow: {:.2}ms", + avg_time + ); server_process.kill().unwrap(); } @@ -222,7 +266,8 @@ fn test_full_request_lifecycle() { let cert_path = temp_dir.path().join("cert.pem"); let key_path = temp_dir.path().join("key.pem"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -231,10 +276,11 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - root_dir.display(), - cert_path.display(), - key_path.display()); + port, + root_dir.display(), + cert_path.display(), + key_path.display() + ); std::fs::write(&config_path, config_content).unwrap(); // Start server with TLS @@ -248,27 +294,64 @@ key = "{}" // Test root index let root_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/"); - assert!(root_response.starts_with("20"), "Root request failed: {}", root_response); - assert!(root_response.contains("Main site content"), "Wrong root content: {}", root_response); + assert!( + root_response.starts_with("20"), + "Root request failed: {}", + root_response + ); + assert!( + root_response.contains("Main site content"), + "Wrong root content: {}", + root_response + ); // Test explicit index let index_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/index.gmi"); - assert!(index_response.starts_with("20"), "Index request failed: {}", index_response); - assert!(index_response.contains("Main site content"), "Wrong index content: {}", index_response); + assert!( + index_response.starts_with("20"), + "Index request failed: {}", + index_response + ); + assert!( + index_response.contains("Main site content"), + "Wrong index content: {}", + index_response + ); // Test subdirectory index let blog_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/blog/"); - assert!(blog_response.starts_with("20"), "Blog request failed: {}", blog_response); - assert!(blog_response.contains("Blog index content"), "Wrong blog content: {}", blog_response); + assert!( + blog_response.starts_with("20"), + "Blog request failed: {}", + blog_response + ); + assert!( + blog_response.contains("Blog index content"), + "Wrong blog content: {}", + blog_response + ); // Test individual file let about_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/about.gmi"); - assert!(about_response.starts_with("20"), "About request failed: {}", about_response); - assert!(about_response.contains("About page content"), "Wrong about content: {}", about_response); + assert!( + about_response.starts_with("20"), + "About request failed: {}", + about_response + ); + assert!( + about_response.contains("About page content"), + "Wrong about content: {}", + about_response + ); // Test not found - let notfound_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/nonexistent.gmi"); - assert!(notfound_response.starts_with("51"), "Not found should return 51: {}", notfound_response); + let notfound_response = + make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/nonexistent.gmi"); + assert!( + notfound_response.starts_with("51"), + "Not found should return 51: {}", + notfound_response + ); server_process.kill().unwrap(); } @@ -295,4 +378,3 @@ fn make_gemini_request(host: &str, port: u16, url: &str) -> String { Err(e) => format!("Error: Failed to run Python client: {}", e), } } - diff --git a/tests/virtual_host_paths.rs b/tests/virtual_host_paths.rs index 7c604b3..528b2f0 100644 --- a/tests/virtual_host_paths.rs +++ b/tests/virtual_host_paths.rs @@ -1,7 +1,5 @@ mod common; - - #[test] fn test_per_host_content_isolation() { let temp_dir = common::setup_test_environment(); @@ -19,7 +17,8 @@ fn test_per_host_content_isolation() { // Create config with two hosts let config_path = temp_dir.path().join("config.toml"); let port = 1965 + (std::process::id() % 1000) as u16; // Use dynamic port - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -33,13 +32,14 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - site1_root.display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display(), - site2_root.display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); + port, + site1_root.display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display(), + site2_root.display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, config_content).unwrap(); // Start server with TLS @@ -54,13 +54,29 @@ key = "{}" // Test site1.com serves its content let response1 = make_gemini_request("127.0.0.1", port, "gemini://site1.com/"); - assert!(response1.starts_with("20"), "Expected success for site1.com, got: {}", response1); - assert!(response1.contains("Welcome to Site 1"), "Should serve site1 content, got: {}", response1); + assert!( + response1.starts_with("20"), + "Expected success for site1.com, got: {}", + response1 + ); + assert!( + response1.contains("Welcome to Site 1"), + "Should serve site1 content, got: {}", + response1 + ); // Test site2.org serves its content let response2 = make_gemini_request("127.0.0.1", port, "gemini://site2.org/"); - assert!(response2.starts_with("20"), "Expected success for site2.org, got: {}", response2); - assert!(response2.contains("Welcome to Site 2"), "Should serve site2 content, got: {}", response2); + assert!( + response2.starts_with("20"), + "Expected success for site2.org, got: {}", + response2 + ); + assert!( + response2.contains("Welcome to Site 2"), + "Should serve site2 content, got: {}", + response2 + ); server_process.kill().unwrap(); } @@ -73,7 +89,11 @@ fn test_per_host_path_security() { let site1_root = temp_dir.path().join("site1"); std::fs::create_dir(&site1_root).unwrap(); std::fs::create_dir(site1_root.join("subdir")).unwrap(); - std::fs::write(site1_root.join("subdir").join("secret.gmi"), "Secret content").unwrap(); + std::fs::write( + site1_root.join("subdir").join("secret.gmi"), + "Secret content", + ) + .unwrap(); // Create config let config_path = temp_dir.path().join("config.toml"); @@ -81,7 +101,8 @@ fn test_per_host_path_security() { let cert_path = temp_dir.path().join("cert.pem"); let key_path = temp_dir.path().join("key.pem"); - let config_content = format!(r#" + let config_content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -90,10 +111,11 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - site1_root.display(), - cert_path.display(), - key_path.display()); + port, + site1_root.display(), + cert_path.display(), + key_path.display() + ); std::fs::write(&config_path, config_content).unwrap(); let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux")) @@ -106,12 +128,24 @@ key = "{}" // Test path traversal attempt should be blocked let response = make_gemini_request("127.0.0.1", port, "gemini://site1.com/../../../etc/passwd"); - assert!(response.starts_with("51"), "Path traversal should be blocked, got: {}", response); + assert!( + response.starts_with("51"), + "Path traversal should be blocked, got: {}", + response + ); // Test valid subdirectory access should work let response = make_gemini_request("127.0.0.1", port, "gemini://site1.com/subdir/secret.gmi"); - assert!(response.starts_with("20"), "Valid subdirectory access should work, got: {}", response); - assert!(response.contains("Secret content"), "Should serve content from subdirectory, got: {}", response); + assert!( + response.starts_with("20"), + "Valid subdirectory access should work, got: {}", + response + ); + assert!( + response.contains("Secret content"), + "Should serve content from subdirectory, got: {}", + response + ); server_process.kill().unwrap(); } @@ -128,4 +162,4 @@ fn make_gemini_request(host: &str, port: u16, url: &str) -> String { .output() .unwrap(); String::from_utf8(output.stdout).unwrap() -} \ No newline at end of file +} diff --git a/tests/virtual_host_routing.rs b/tests/virtual_host_routing.rs index 520ac88..56e00b1 100644 --- a/tests/virtual_host_routing.rs +++ b/tests/virtual_host_routing.rs @@ -71,7 +71,8 @@ fn test_virtual_host_routing_multiple_hosts() { // Create config with two hosts let config_path = temp_dir.path().join("config.toml"); - let content = format!(r#" + let content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -85,20 +86,29 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - temp_dir.path().join("site1").display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display(), - temp_dir.path().join("site2").display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); + port, + temp_dir.path().join("site1").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display(), + temp_dir.path().join("site2").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, content).unwrap(); // Create host-specific content std::fs::create_dir_all(temp_dir.path().join("site1")).unwrap(); std::fs::create_dir_all(temp_dir.path().join("site2")).unwrap(); - std::fs::write(temp_dir.path().join("site1").join("index.gmi"), "# Site 1 Content\n").unwrap(); - std::fs::write(temp_dir.path().join("site2").join("index.gmi"), "# Site 2 Content\n").unwrap(); + std::fs::write( + temp_dir.path().join("site1").join("index.gmi"), + "# Site 1 Content\n", + ) + .unwrap(); + std::fs::write( + temp_dir.path().join("site2").join("index.gmi"), + "# Site 2 Content\n", + ) + .unwrap(); // Use the same certs for both hosts (server uses first cert anyway) @@ -114,11 +124,19 @@ key = "{}" // Test request to site1.com with TLS let response1 = make_gemini_request("127.0.0.1", port, "gemini://site1.com/index.gmi"); - assert!(response1.starts_with("20"), "Expected success response for site1.com, got: {}", response1); + assert!( + response1.starts_with("20"), + "Expected success response for site1.com, got: {}", + response1 + ); // Test request to site2.org let response2 = make_gemini_request("127.0.0.1", port, "gemini://site2.org/index.gmi"); - assert!(response2.starts_with("20"), "Expected success response for site2.org, got: {}", response2); + assert!( + response2.starts_with("20"), + "Expected success response for site2.org, got: {}", + response2 + ); server_process.kill().unwrap(); } @@ -132,7 +150,8 @@ fn test_virtual_host_routing_known_hostname() { // Config with only one host let config_path = temp_dir.path().join("config.toml"); - let content = format!(r#" + let content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -141,10 +160,11 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - temp_dir.path().join("content").display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); + port, + temp_dir.path().join("content").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, content).unwrap(); // Start server with TLS @@ -159,7 +179,11 @@ key = "{}" // Test request to unknown hostname let response = make_gemini_request("127.0.0.1", port, "gemini://unknown.com/index.gmi"); - assert!(response.starts_with("53"), "Should return status 53 for unknown hostname, got: {}", response); + assert!( + response.starts_with("53"), + "Should return status 53 for unknown hostname, got: {}", + response + ); server_process.kill().unwrap(); } @@ -173,7 +197,8 @@ fn test_virtual_host_routing_malformed_url() { // Config with one host let config_path = temp_dir.path().join("config.toml"); - let content = format!(r#" + let content = format!( + r#" bind_host = "127.0.0.1" port = {} @@ -182,10 +207,11 @@ root = "{}" cert = "{}" key = "{}" "#, - port, - temp_dir.path().join("content").display(), - temp_dir.path().join("cert.pem").display(), - temp_dir.path().join("key.pem").display()); + port, + temp_dir.path().join("content").display(), + temp_dir.path().join("cert.pem").display(), + temp_dir.path().join("key.pem").display() + ); std::fs::write(&config_path, content).unwrap(); // Start server with TLS @@ -200,7 +226,11 @@ key = "{}" // Test malformed URL (wrong protocol) let response = make_gemini_request("127.0.0.1", port, "http://example.com/index.gmi"); - assert!(response.starts_with("59"), "Should return status 59 for malformed URL, got: {}", response); + assert!( + response.starts_with("59"), + "Should return status 59 for malformed URL, got: {}", + response + ); server_process.kill().unwrap(); -} \ No newline at end of file +}