- Replace panic-prone config loading with detailed error messages - Validate config file existence, TOML syntax, required fields - Check filesystem access for root directory and certificate files - Provide actionable error messages explaining how to fix each issue - Exit gracefully with clear guidance instead of cryptic panics - Maintain backward compatibility for valid configurations
208 lines
No EOL
7.6 KiB
Rust
208 lines
No EOL
7.6 KiB
Rust
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<String>,
|
|
|
|
/// TESTING ONLY: Add delay before processing (seconds) [debug builds only]
|
|
#[cfg(debug_assertions)]
|
|
#[arg(long, value_name = "SECONDS")]
|
|
test_processing_delay: Option<u64>,
|
|
}
|
|
|
|
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} |