diff --git a/dist/INSTALL.md b/dist/INSTALL.md new file mode 100644 index 0000000..8d5caa3 --- /dev/null +++ b/dist/INSTALL.md @@ -0,0 +1,223 @@ +# Installing Pollux Gemini Server + +This guide covers installing and configuring the Pollux Gemini server for production use. + +## Prerequisites + +- Linux system with systemd +- Rust toolchain (for building from source) +- Domain name with DNS configured +- Let's Encrypt account (for certificates) + +## Quick Start + +```bash +# 1. Build and install +cargo build --release +sudo cp target/release/pollux /usr/local/bin/ + +# 2. Get certificates +sudo certbot certonly --standalone -d example.com + +# 3. Create directories and user +sudo useradd -r -s /bin/false gemini +sudo usermod -a -G ssl-cert gemini +sudo mkdir -p /etc/pollux /var/www/example.com +sudo chown -R gemini:gemini /var/www/example.com + +# 4. Install config +sudo cp dist/config.toml /etc/pollux/ + +# 5. Add your Gemini content +sudo cp -r your-content/* /var/www/example.com/ + +# 6. Install and start service +sudo cp dist/pollux.service /etc/systemd/system/ +sudo systemctl daemon-reload +sudo systemctl enable pollux +sudo systemctl start pollux + +# 7. Check status +sudo systemctl status pollux +sudo journalctl -u pollux -f +``` + +## Detailed Installation + +### Building from Source + +```bash +git clone https://github.com/yourusername/pollux.git +cd pollux +cargo build --release +sudo cp target/release/pollux /usr/local/bin/ +``` + +### Certificate Setup + +#### Let's Encrypt (Recommended) + +```bash +# Install certbot +sudo apt install certbot # Ubuntu/Debian +# OR +sudo dnf install certbot # Fedora/RHEL + +# Get certificate +sudo certbot certonly --standalone -d example.com + +# Verify permissions +ls -la /etc/letsencrypt/live/example.com/ +# Should show fullchain.pem and privkey.pem +``` + +#### Self-Signed (Development Only) + +```bash +# Generate certificates +openssl req -x509 -newkey rsa:4096 \ + -keyout /etc/pollux/key.pem \ + -out /etc/pollux/cert.pem \ + -days 365 -nodes \ + -subj "/CN=example.com" + +# Set permissions +sudo chown gemini:gemini /etc/pollux/*.pem +sudo chmod 644 /etc/pollux/cert.pem +sudo chmod 600 /etc/pollux/key.pem +``` + +### User and Directory Setup + +```bash +# Create service user +sudo useradd -r -s /bin/false gemini + +# Add to certificate group (varies by distro) +sudo usermod -a -G ssl-cert gemini # Ubuntu/Debian +# OR +sudo usermod -a -G certbot gemini # Some systems + +# Create directories +sudo mkdir -p /etc/pollux /var/www/example.com +sudo chown -R gemini:gemini /var/www/example.com +``` + +### Configuration + +Edit `/etc/pollux/config.toml`: + +```toml +root = "/var/www/example.com" +cert = "/etc/letsencrypt/live/example.com/fullchain.pem" +key = "/etc/letsencrypt/live/example.com/privkey.pem" +bind_host = "0.0.0.0" +hostname = "example.com" +port = 1965 +max_concurrent_requests = 1000 +log_level = "info" +``` + +### Content Setup + +```bash +# Copy your Gemini files +sudo cp -r gemini-content/* /var/www/example.com/ + +# Set permissions +sudo chown -R gemini:gemini /var/www/example.com +sudo find /var/www/example.com -type f -exec chmod 644 {} \; +sudo find /var/www/example.com -type d -exec chmod 755 {} \; +``` + +### Service Installation + +```bash +# Install service file +sudo cp dist/pollux.service /etc/systemd/system/ + +# If your paths differ, edit the service file +sudo editor /etc/systemd/system/pollux.service +# Update ReadOnlyPaths to match your config + +# Enable and start +sudo systemctl daemon-reload +sudo systemctl enable pollux +sudo systemctl start pollux +``` + +### Verification + +```bash +# Check service status +sudo systemctl status pollux + +# View logs +sudo journalctl -u pollux -f + +# Test connection +openssl s_client -connect example.com:1965 -servername example.com <<< "gemini://example.com/\r\n" | head -1 +``` + +## Troubleshooting + +### Permission Issues +```bash +# Check certificate access +sudo -u gemini cat /etc/letsencrypt/live/example.com/fullchain.pem + +# Check content access +sudo -u gemini ls -la /var/www/example.com/ +``` + +### Port Issues +```bash +# Check if port is in use +sudo netstat -tlnp | grep :1965 + +# Test binding +sudo -u gemini /usr/local/bin/pollux # Should show startup messages +``` + +### Certificate Issues +```bash +# Renew certificates +sudo certbot renew + +# Reload service after cert renewal +sudo systemctl reload pollux +``` + +## Configuration Options + +See `config.toml` for all available options. Key settings: + +- `root`: Directory containing your .gmi files +- `cert`/`key`: TLS certificate paths +- `bind_host`: IP/interface to bind to +- `hostname`: Domain name for URI validation +- `port`: Listen port (1965 is standard) +- `max_concurrent_requests`: Connection limit +- `log_level`: Logging verbosity + +## Upgrading + +```bash +# Stop service +sudo systemctl stop pollux + +# Install new binary +sudo cp target/release/pollux /usr/local/bin/ + +# Start service +sudo systemctl start pollux +``` + +## Security Notes + +- Certificates are read-only by the service user +- Content directory is read-only +- No temporary file access +- Systemd security hardening applied +- Private keys have restricted permissions +- URI validation prevents domain confusion attacks \ No newline at end of file diff --git a/dist/config.toml b/dist/config.toml new file mode 100644 index 0000000..ab7067a --- /dev/null +++ b/dist/config.toml @@ -0,0 +1,68 @@ +# Pollux Gemini Server Configuration +# +# This is an example configuration file for the Pollux Gemini server. +# Copy this file to /etc/pollux/config.toml and customize the values below. +# +# The Gemini protocol is specified in RFC 1436: https://tools.ietf.org/rfc/rfc1436.txt + +# Directory containing your Gemini files (.gmi, .txt, images, etc.) +# The server will serve files from this directory and its subdirectories. +# Default index file is 'index.gmi' for directory requests. +# +# IMPORTANT: The server needs READ access to this directory. +# Make sure the service user (gemini) can read all files here. +root = "/var/www/example.com" + +# TLS certificate and private key files +# These files are required for TLS encryption (Gemini requires TLS). +# +# For Let's Encrypt certificates (recommended for production): +# cert = "/etc/letsencrypt/live/example.com/fullchain.pem" +# key = "/etc/letsencrypt/live/example.com/privkey.pem" +# +# To obtain Let's Encrypt certs: +# sudo certbot certonly --standalone -d example.com +# +# For development/testing, generate self-signed certs: +# openssl req -x509 -newkey rsa:4096 -keyout /etc/pollux/key.pem -out /etc/pollux/cert.pem -days 365 -nodes -subj "/CN=example.com" +cert = "/etc/letsencrypt/live/example.com/fullchain.pem" +key = "/etc/letsencrypt/live/example.com/privkey.pem" + +# Server network configuration +# +# bind_host: IP address or interface to bind the server to +# - "0.0.0.0" = listen on all interfaces (default) +# - "127.0.0.1" = localhost only +# - "::" = IPv6 all interfaces +# - Specific IP = bind to that address only +bind_host = "0.0.0.0" + +# hostname: Domain name for URI validation +# - Used to validate incoming gemini:// URIs +# - Clients must use: gemini://yourdomain.com +# - Server validates that requests match this hostname +hostname = "example.com" + +# port: TCP port to listen on +# - Default Gemini port is 1965 +# - Ports below 1024 require root privileges +# - Choose a different port if 1965 is in use +port = 1965 + +# Request limiting +# +# max_concurrent_requests: Maximum number of simultaneous connections +# - Prevents server overload and DoS attacks +# - Set to 0 to disable limiting (not recommended) +# - Typical values: 100-10000 depending on server capacity +max_concurrent_requests = 1000 + +# Logging configuration +# +# log_level: Controls how much information is logged +# - "error": Only errors that prevent normal operation +# - "warn": Errors plus warnings about unusual conditions +# - "info": General operational information (recommended) +# - "debug": Detailed debugging information +# - "trace": Very verbose debugging (use only for troubleshooting) +log_level = "info" \ No newline at end of file diff --git a/dist/pollux.service b/dist/pollux.service new file mode 100644 index 0000000..a05eb16 --- /dev/null +++ b/dist/pollux.service @@ -0,0 +1,24 @@ +[Unit] +Description=Pollux Gemini Server +After=network.target +Wants=network.target + +[Service] +Type=simple +ExecStart=/usr/local/bin/pollux +ExecReload=/bin/kill -HUP $MAINPID +Restart=on-failure +RestartSec=5 +User=gemini +Group=gemini +NoNewPrivileges=yes +ProtectHome=yes +ProtectSystem=strict +ReadOnlyPaths=/etc/pollux /etc/letsencrypt/live/example.com /var/www/example.com +# NOTE: Adjust paths to match your config: +# - /etc/letsencrypt/live/example.com for Let's Encrypt certs +# - /var/www/example.com for your content root +# The server needs read access to config, certificates, and content files + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 8342953..c3a546b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,7 +5,8 @@ pub struct Config { pub root: Option, pub cert: Option, pub key: Option, - pub host: Option, + pub bind_host: Option, + pub hostname: Option, pub port: Option, pub log_level: Option, pub max_concurrent_requests: Option, @@ -31,7 +32,8 @@ mod tests { root = "/path/to/root" cert = "cert.pem" key = "key.pem" - host = "example.com" + bind_host = "0.0.0.0" + hostname = "example.com" port = 1965 log_level = "info" "#; @@ -41,7 +43,8 @@ mod tests { assert_eq!(config.root, Some("/path/to/root".to_string())); assert_eq!(config.cert, Some("cert.pem".to_string())); assert_eq!(config.key, Some("key.pem".to_string())); - assert_eq!(config.host, Some("example.com".to_string())); + assert_eq!(config.bind_host, Some("0.0.0.0".to_string())); + assert_eq!(config.hostname, Some("example.com".to_string())); assert_eq!(config.port, Some(1965)); assert_eq!(config.log_level, Some("info".to_string())); assert_eq!(config.max_concurrent_requests, None); // Default diff --git a/src/main.rs b/src/main.rs index be856cf..5ea2c67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -52,7 +52,8 @@ async fn main() { root: None, cert: None, key: None, - host: None, + bind_host: None, + hostname: None, port: None, log_level: None, max_concurrent_requests: None, @@ -66,7 +67,8 @@ async fn main() { let root = config.root.expect("root is required"); let cert_path = config.cert.expect("cert is required"); let key_path = config.key.expect("key is required"); - let host = config.host.unwrap_or_else(|| "0.0.0.0".to_string()); + let bind_host = config.bind_host.unwrap_or_else(|| "0.0.0.0".to_string()); + let hostname = config.hostname.expect("hostname is required"); let port = config.port.unwrap_or(1965); // Validate max concurrent requests @@ -104,22 +106,22 @@ async fn main() { let acceptor = TlsAcceptor::from(Arc::new(config)); - let listener = TcpListener::bind(format!("{}:{}", host, port)).await.unwrap(); + let listener = TcpListener::bind(format!("{}:{}", bind_host, port)).await.unwrap(); // Print startup information - print_startup_info(&host, port, &root, &cert_path, &key_path, Some(log_level), max_concurrent_requests); + 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_host = "localhost".to_string(); // Override for testing + let expected_hostname = hostname.clone(); // Use configured hostname 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_host, port, max_concurrent, test_delay).await { + if let Err(e) = server::handle_connection(stream, &dir, &expected_hostname, port, max_concurrent, test_delay).await { tracing::error!("Error handling connection: {}", e); } } diff --git a/src/request.rs b/src/request.rs index 9a57584..bf939bd 100644 --- a/src/request.rs +++ b/src/request.rs @@ -6,7 +6,7 @@ pub enum PathResolutionError { NotFound, } -pub fn parse_gemini_url(request: &str, expected_host: &str, expected_port: u16) -> Result { +pub fn parse_gemini_url(request: &str, hostname: &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]; @@ -21,7 +21,7 @@ pub fn parse_gemini_url(request: &str, expected_host: &str, expected_port: u16) }; // Validate host - if host != expected_host { + if host != hostname { return Err(()); // Hostname mismatch } diff --git a/src/server.rs b/src/server.rs index d1481c6..1e5e577 100644 --- a/src/server.rs +++ b/src/server.rs @@ -40,7 +40,7 @@ pub async fn serve_file( pub async fn handle_connection( mut stream: TlsStream, dir: &str, - expected_host: &str, + hostname: &str, expected_port: u16, max_concurrent_requests: usize, _test_processing_delay: u64, @@ -97,7 +97,7 @@ pub async fn handle_connection( } // Parse Gemini URL - let path = match parse_gemini_url(&request, expected_host, expected_port) { + let path = match parse_gemini_url(&request, hostname, expected_port) { Ok(p) => p, Err(_) => { logger.log_error(59, "Invalid URL format");