mod config; mod tls; mod request; mod server; mod logging; use clap::Parser; use rustls::ServerConfig; use std::path::Path; use std::sync::Arc; use tokio::net::TcpListener; use tokio_rustls::TlsAcceptor; use logging::init_logging; fn print_startup_info(host: &str, port: u16, root: &str, cert: &str, key: &str, log_level: Option<&str>, max_concurrent: usize) { println!("Pollux Gemini Server"); println!("Listening on: {}:{}", host, port); println!("Serving: {}", root); println!("Certificate: {}", cert); println!("Key: {}", key); println!("Max concurrent requests: {}", max_concurrent); if let Some(level) = log_level { println!("Log level: {}", level); } println!(); // Add spacing before connections start } #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Args { /// Path to config file #[arg(short = 'C', long)] config: Option, /// TESTING ONLY: Add delay before processing (seconds) [debug builds only] #[cfg(debug_assertions)] #[arg(long, value_name = "SECONDS")] test_processing_delay: Option, } #[tokio::main] async fn main() { let args = Args::parse(); // Load config let config_path = args.config.as_deref().unwrap_or("/etc/pollux/config.toml"); // Check if config file exists if !std::path::Path::new(&config_path).exists() { eprintln!("Error: Config file '{}' not found", config_path); eprintln!("Create the config file with required fields:"); eprintln!(" root = \"/path/to/gemini/content\""); eprintln!(" cert = \"/path/to/certificate.pem\""); eprintln!(" key = \"/path/to/private-key.pem\""); eprintln!(" bind_host = \"0.0.0.0\""); eprintln!(" hostname = \"your.domain.com\""); std::process::exit(1); } // 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 all values are properly quoted."); std::process::exit(1); } }; // Validate required fields if config.root.is_none() { eprintln!("Error: 'root' field is required in config file"); eprintln!("Add: root = \"/path/to/gemini/content\""); std::process::exit(1); } if config.cert.is_none() { eprintln!("Error: 'cert' field is required in config file"); eprintln!("Add: cert = \"/path/to/certificate.pem\""); std::process::exit(1); } if config.key.is_none() { eprintln!("Error: 'key' field is required in config file"); eprintln!("Add: key = \"/path/to/private-key.pem\""); std::process::exit(1); } if config.hostname.is_none() { eprintln!("Error: 'hostname' field is required in config file"); eprintln!("Add: hostname = \"your.domain.com\""); std::process::exit(1); } // Validate filesystem let root_path = std::path::Path::new(config.root.as_ref().unwrap()); if !root_path.exists() { eprintln!("Error: Root directory '{}' does not exist", config.root.as_ref().unwrap()); eprintln!("Create the directory and add your Gemini files (.gmi, .txt, images)"); std::process::exit(1); } if !root_path.is_dir() { eprintln!("Error: Root path '{}' is not a directory", config.root.as_ref().unwrap()); eprintln!("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 '{}': {}", config.root.as_ref().unwrap(), e); eprintln!("Ensure the directory exists and the server user has read permission"); std::process::exit(1); } let cert_path = std::path::Path::new(config.cert.as_ref().unwrap()); if !cert_path.exists() { eprintln!("Error: Certificate file '{}' does not exist", config.cert.as_ref().unwrap()); 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 '{}': {}", config.cert.as_ref().unwrap(), e); eprintln!("Ensure the file exists and the server user has read permission"); std::process::exit(1); } let key_path = std::path::Path::new(config.key.as_ref().unwrap()); if !key_path.exists() { eprintln!("Error: Private key file '{}' does not exist", config.key.as_ref().unwrap()); eprintln!("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 '{}': {}", config.key.as_ref().unwrap(), e); eprintln!("Ensure the file exists and the server user has read permission"); std::process::exit(1); } // Initialize logging after config validation let log_level = config.log_level.as_deref().unwrap_or("info"); init_logging(log_level); // Extract validated config values let root = config.root.unwrap(); let cert_path = config.cert.unwrap(); let key_path = config.key.unwrap(); let bind_host = config.bind_host.unwrap_or_else(|| "0.0.0.0".to_string()); let hostname = config.hostname.unwrap(); let port = config.port.unwrap_or(1965); // 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"); std::process::exit(1); } // TESTING ONLY: Read delay argument (debug builds only) #[cfg(debug_assertions)] let test_processing_delay = args.test_processing_delay .filter(|&d| d > 0 && d <= 300) .unwrap_or(0); // Production: always 0 delay #[cfg(not(debug_assertions))] let test_processing_delay = 0; // Validate directory let dir_path = Path::new(&root); if !dir_path.exists() || !dir_path.is_dir() { eprintln!("Error: Directory '{}' does not exist or is not a directory", root); std::process::exit(1); } // Load TLS certificates let certs = tls::load_certs(&cert_path).unwrap(); let key = tls::load_private_key(&key_path).unwrap(); let config = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(certs, key).unwrap(); let acceptor = TlsAcceptor::from(Arc::new(config)); let listener = TcpListener::bind(format!("{}:{}", bind_host, port)).await.unwrap(); // Print startup information print_startup_info(&bind_host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests); loop { let (stream, _) = listener.accept().await.unwrap(); tracing::debug!("Accepted connection from {}", stream.peer_addr().unwrap_or_else(|_| "unknown".parse().unwrap())); let acceptor = acceptor.clone(); let dir = root.clone(); let expected_hostname = hostname.clone(); let max_concurrent = max_concurrent_requests; let test_delay = test_processing_delay; tokio::spawn(async move { if let Ok(stream) = acceptor.accept(stream).await { if let Err(e) = server::handle_connection(stream, &dir, &expected_hostname, port, max_concurrent, test_delay).await { tracing::error!("Error handling connection: {}", e); } } }); } }