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
This commit is contained in:
parent
50a4d9bc75
commit
55fe47b172
15 changed files with 787 additions and 459 deletions
19
README.md
19
README.md
|
|
@ -115,9 +115,26 @@ Access with a Gemini client like Lagrange at `gemini://yourdomain.com/`.
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
- `--config` (`-C`): Path to config file (default `/etc/pollux/config.toml`)
|
- `--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
|
- `--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
|
## Security
|
||||||
|
|
||||||
Pollux is designed with security as a priority:
|
Pollux is designed with security as a priority:
|
||||||
|
|
|
||||||
20
dist/INSTALL.md
vendored
20
dist/INSTALL.md
vendored
|
|
@ -104,7 +104,6 @@ Edit `/etc/pollux/config.toml`:
|
||||||
bind_host = "0.0.0.0"
|
bind_host = "0.0.0.0"
|
||||||
port = 1965
|
port = 1965
|
||||||
max_concurrent_requests = 1000
|
max_concurrent_requests = 1000
|
||||||
log_level = "info"
|
|
||||||
|
|
||||||
# Host configuration
|
# Host configuration
|
||||||
["example.com"]
|
["example.com"]
|
||||||
|
|
@ -113,6 +112,22 @@ cert = "/etc/pollux/tls/cert.pem"
|
||||||
key = "/etc/pollux/tls/key.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
|
### Content Setup
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -194,7 +209,8 @@ See `config.toml` for all available options. Key settings:
|
||||||
- `bind_host`: IP/interface to bind to (global)
|
- `bind_host`: IP/interface to bind to (global)
|
||||||
- `port`: Listen port (1965 is standard, per host override possible)
|
- `port`: Listen port (1965 is standard, per host override possible)
|
||||||
- `max_concurrent_requests`: Connection limit (global)
|
- `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
|
## Certificate Management
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ pub struct Config {
|
||||||
// Global defaults (optional)
|
// Global defaults (optional)
|
||||||
pub bind_host: Option<String>,
|
pub bind_host: Option<String>,
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
pub log_level: Option<String>,
|
|
||||||
pub max_concurrent_requests: Option<usize>,
|
pub max_concurrent_requests: Option<usize>,
|
||||||
|
|
||||||
// Per-hostname configurations
|
// Per-hostname configurations
|
||||||
|
|
@ -21,7 +20,7 @@ pub struct HostConfig {
|
||||||
pub key: String,
|
pub key: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub port: Option<u16>, // override global port
|
pub port: Option<u16>, // override global port
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub log_level: Option<String>, // override global log level
|
pub log_level: Option<String>, // override global log level
|
||||||
|
|
@ -34,7 +33,6 @@ pub fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
|
||||||
// Extract global settings
|
// Extract global settings
|
||||||
let bind_host = extract_string(&toml_value, "bind_host");
|
let bind_host = extract_string(&toml_value, "bind_host");
|
||||||
let port = extract_u16(&toml_value, "port");
|
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");
|
let max_concurrent_requests = extract_usize(&toml_value, "max_concurrent_requests");
|
||||||
|
|
||||||
// Extract host configurations
|
// Extract host configurations
|
||||||
|
|
@ -43,7 +41,10 @@ pub fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
|
||||||
if let Some(table) = toml_value.as_table() {
|
if let Some(table) = toml_value.as_table() {
|
||||||
for (key, value) in table {
|
for (key, value) in table {
|
||||||
// Skip global config keys
|
// 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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -57,7 +58,11 @@ pub fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
// Validate hostname
|
// Validate hostname
|
||||||
if !is_valid_hostname(key) {
|
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
|
// Validate that root directory exists
|
||||||
|
|
@ -96,34 +101,53 @@ pub fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> {
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
bind_host,
|
bind_host,
|
||||||
port,
|
port,
|
||||||
log_level,
|
|
||||||
max_concurrent_requests,
|
max_concurrent_requests,
|
||||||
hosts,
|
hosts,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_string(value: &Value, key: &str) -> Option<String> {
|
fn extract_string(value: &Value, key: &str) -> Option<String> {
|
||||||
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<String, Value>, key: &str) -> Option<String> {
|
fn extract_string_from_table(table: &toml::map::Map<String, Value>, key: &str) -> Option<String> {
|
||||||
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<u16> {
|
fn extract_u16(value: &Value, key: &str) -> Option<u16> {
|
||||||
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<String, Value>, key: &str) -> Option<u16> {
|
fn extract_u16_from_table(table: &toml::map::Map<String, Value>, key: &str) -> Option<u16> {
|
||||||
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<usize> {
|
fn extract_usize(value: &Value, key: &str) -> Option<usize> {
|
||||||
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<String, Value>, key: &str, section: &str) -> Result<String, Box<dyn std::error::Error>> {
|
fn extract_required_string(
|
||||||
table.get(key)
|
table: &toml::map::Map<String, Value>,
|
||||||
|
key: &str,
|
||||||
|
section: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
table
|
||||||
|
.get(key)
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
.ok_or_else(|| format!("Missing required field '{}' in [{}] section", key, section).into())
|
.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("invalid.com."));
|
assert!(!is_valid_hostname("invalid.com."));
|
||||||
assert!(!is_valid_hostname("inval!d.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]
|
#[test]
|
||||||
|
|
@ -220,14 +246,19 @@ mod tests {
|
||||||
fs::write(&cert_path, "dummy cert").unwrap();
|
fs::write(&cert_path, "dummy cert").unwrap();
|
||||||
fs::write(&key_path, "dummy key").unwrap();
|
fs::write(&key_path, "dummy key").unwrap();
|
||||||
|
|
||||||
let content = format!(r#"
|
let content = format!(
|
||||||
|
r#"
|
||||||
["example.com"]
|
["example.com"]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
port = 1965
|
port = 1965
|
||||||
log_level = "info"
|
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();
|
fs::write(&config_path, content).unwrap();
|
||||||
|
|
||||||
let config = load_config(config_path.to_str().unwrap()).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_cert, "dummy cert 2").unwrap();
|
||||||
fs::write(&site2_key, "dummy key 2").unwrap();
|
fs::write(&site2_key, "dummy key 2").unwrap();
|
||||||
|
|
||||||
let content = format!(r#"
|
let content = format!(
|
||||||
|
r#"
|
||||||
["site1.com"]
|
["site1.com"]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
|
|
@ -273,8 +305,14 @@ mod tests {
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
port = 1966
|
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();
|
fs::write(&config_path, content).unwrap();
|
||||||
|
|
||||||
let config = load_config(config_path.to_str().unwrap()).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());
|
let result = load_config(config_path.to_str().unwrap());
|
||||||
assert!(result.is_err());
|
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]
|
#[test]
|
||||||
|
|
@ -347,4 +388,4 @@ mod tests {
|
||||||
// Config parsing will fail if required fields are missing
|
// Config parsing will fail if required fields are missing
|
||||||
assert!(load_config(config_path.to_str().unwrap()).is_err());
|
assert!(load_config(config_path.to_str().unwrap()).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
107
src/logging.rs
107
src/logging.rs
|
|
@ -1,106 +1,5 @@
|
||||||
use tokio::net::TcpStream;
|
// Logging module - now unused as logging is handled directly in main.rs
|
||||||
use std::time::Instant;
|
// All logging functionality moved to main.rs with RUST_LOG environment variable support
|
||||||
use tokio_rustls::server::TlsStream;
|
|
||||||
use tracing_subscriber::fmt::format::Writer;
|
|
||||||
use tracing_subscriber::fmt::FormatFields;
|
|
||||||
|
|
||||||
struct CleanLogFormatter;
|
|
||||||
|
|
||||||
impl<S, N> tracing_subscriber::fmt::FormatEvent<S, N> 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<TcpStream>, 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<TcpStream>) -> 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
@ -109,4 +8,4 @@ mod tests {
|
||||||
// Basic test to ensure logging module compiles
|
// Basic test to ensure logging module compiles
|
||||||
assert!(true);
|
assert!(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
144
src/main.rs
144
src/main.rs
|
|
@ -1,8 +1,8 @@
|
||||||
mod config;
|
mod config;
|
||||||
mod tls;
|
mod logging;
|
||||||
mod request;
|
mod request;
|
||||||
mod server;
|
mod server;
|
||||||
mod logging;
|
mod tls;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use rustls::ServerConfig;
|
use rustls::ServerConfig;
|
||||||
|
|
@ -10,9 +10,11 @@ use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
use tokio_rustls::TlsAcceptor;
|
use tokio_rustls::TlsAcceptor;
|
||||||
use logging::init_logging;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
fn create_tls_config(hosts: &std::collections::HashMap<String, config::HostConfig>) -> Result<Arc<ServerConfig>, Box<dyn std::error::Error>> {
|
fn create_tls_config(
|
||||||
|
hosts: &std::collections::HashMap<String, config::HostConfig>,
|
||||||
|
) -> Result<Arc<ServerConfig>, Box<dyn std::error::Error>> {
|
||||||
// For Phase 3, we'll use the first host's certificate for all connections
|
// For Phase 3, we'll use the first host's certificate for all connections
|
||||||
// TODO: Phase 4 could implement proper SNI-based certificate selection
|
// TODO: Phase 4 could implement proper SNI-based certificate selection
|
||||||
let first_host = hosts.values().next().ok_or("No hosts configured")?;
|
let first_host = hosts.values().next().ok_or("No hosts configured")?;
|
||||||
|
|
@ -28,7 +30,10 @@ fn create_tls_config(hosts: &std::collections::HashMap<String, config::HostConfi
|
||||||
Ok(Arc::new(config))
|
Ok(Arc::new(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_startup_info(config: &config::Config, hosts: &std::collections::HashMap<String, config::HostConfig>) {
|
fn print_startup_info(
|
||||||
|
config: &config::Config,
|
||||||
|
hosts: &std::collections::HashMap<String, config::HostConfig>,
|
||||||
|
) {
|
||||||
println!("Pollux Gemini Server (Virtual Host Mode)");
|
println!("Pollux Gemini Server (Virtual Host Mode)");
|
||||||
println!("Configured hosts:");
|
println!("Configured hosts:");
|
||||||
for (hostname, host_config) in 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 {
|
if let Some(port) = config.port {
|
||||||
println!(" Default port: {}", 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 {
|
if let Some(max_concurrent) = config.max_concurrent_requests {
|
||||||
println!(" Max concurrent requests: {}", max_concurrent);
|
println!(" Max concurrent requests: {}", max_concurrent);
|
||||||
}
|
}
|
||||||
println!(); // Add spacing before connections start
|
println!(); // Add spacing before connections start
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
struct Args {
|
struct Args {
|
||||||
|
|
@ -64,8 +65,6 @@ struct Args {
|
||||||
test_processing_delay: Option<u64>,
|
test_processing_delay: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
@ -73,21 +72,41 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Load config
|
// Load config
|
||||||
let config_path = args.config.as_deref().unwrap_or("/etc/pollux/config.toml");
|
let config_path = args.config.as_deref().unwrap_or("/etc/pollux/config.toml");
|
||||||
if !std::path::Path::new(&config_path).exists() {
|
if !std::path::Path::new(&config_path).exists() {
|
||||||
eprintln!("Error: Config file '{}' not found", config_path);
|
// User guidance goes to stdout BEFORE initializing tracing
|
||||||
eprintln!("Create the config file with virtual host sections like:");
|
// Use direct stderr for error, stdout for guidance
|
||||||
eprintln!("[example.com]");
|
use std::io::Write;
|
||||||
eprintln!("root = \"/srv/gemini/example.com/gemini/\"");
|
let mut stderr = std::io::stderr();
|
||||||
eprintln!("cert = \"/srv/gemini/example.com/tls/fullchain.pem\"");
|
let mut stdout = std::io::stdout();
|
||||||
eprintln!("key = \"/srv/gemini/example.com/tls/privkey.pem\"");
|
|
||||||
|
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);
|
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
|
// Load and parse config
|
||||||
let config = match config::load_config(config_path) {
|
let config = match config::load_config(config_path) {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Error: Failed to parse config file '{}': {}", config_path, e);
|
tracing::error!("Failed to parse config file '{}': {}", config_path, e);
|
||||||
eprintln!("Check the TOML syntax and ensure host sections are properly formatted.");
|
tracing::error!(
|
||||||
|
"Check the TOML syntax and ensure host sections are properly formatted."
|
||||||
|
);
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -97,61 +116,88 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Validate root directory exists and is readable
|
// Validate root directory exists and is readable
|
||||||
let root_path = Path::new(&host_config.root);
|
let root_path = Path::new(&host_config.root);
|
||||||
if !root_path.exists() {
|
if !root_path.exists() {
|
||||||
eprintln!("Error: Root directory '{}' for host '{}' does not exist", host_config.root, hostname);
|
tracing::error!(
|
||||||
eprintln!("Create the directory and add your Gemini files (.gmi, .txt, images)");
|
"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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
if !root_path.is_dir() {
|
if !root_path.is_dir() {
|
||||||
eprintln!("Error: Root path '{}' for host '{}' is not a directory", host_config.root, hostname);
|
tracing::error!(
|
||||||
eprintln!("The 'root' field must point to a directory containing your content");
|
"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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
if let Err(e) = std::fs::read_dir(root_path) {
|
if let Err(e) = std::fs::read_dir(root_path) {
|
||||||
eprintln!("Error: Cannot read root directory '{}' for host '{}': {}", host_config.root, hostname, e);
|
tracing::error!(
|
||||||
eprintln!("Ensure the directory exists and the server user has read permission");
|
"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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate certificate files (always required for TLS)
|
// Validate certificate files (always required for TLS)
|
||||||
let cert_path = Path::new(&host_config.cert);
|
let cert_path = Path::new(&host_config.cert);
|
||||||
if !cert_path.exists() {
|
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");
|
eprintln!("Generate or obtain TLS certificates for your domain");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
if let Err(e) = std::fs::File::open(cert_path) {
|
if let Err(e) = std::fs::File::open(cert_path) {
|
||||||
eprintln!("Error: Cannot read certificate file '{}' for host '{}': {}", host_config.cert, hostname, e);
|
tracing::error!(
|
||||||
eprintln!("Ensure the file exists and the server user has read permission");
|
"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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_path = Path::new(&host_config.key);
|
let key_path = Path::new(&host_config.key);
|
||||||
if !key_path.exists() {
|
if !key_path.exists() {
|
||||||
eprintln!("Error: Private key file '{}' for host '{}' does not exist", host_config.key, hostname);
|
tracing::error!(
|
||||||
eprintln!("Generate or obtain TLS private key for your domain");
|
"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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
if let Err(e) = std::fs::File::open(key_path) {
|
if let Err(e) = std::fs::File::open(key_path) {
|
||||||
eprintln!("Error: Cannot read private key file '{}' for host '{}': {}", host_config.key, hostname, e);
|
tracing::error!(
|
||||||
eprintln!("Ensure the file exists and the server user has read permission");
|
"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);
|
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
|
// Validate max concurrent requests
|
||||||
let max_concurrent_requests = config.max_concurrent_requests.unwrap_or(1000);
|
let max_concurrent_requests = config.max_concurrent_requests.unwrap_or(1000);
|
||||||
if max_concurrent_requests == 0 || max_concurrent_requests > 1_000_000 {
|
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);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TESTING ONLY: Read delay argument (debug builds only)
|
// TESTING ONLY: Read delay argument (debug builds only)
|
||||||
#[cfg(debug_assertions)]
|
#[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)
|
.filter(|&d| d > 0 && d <= 300)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
|
@ -172,7 +218,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let port = config.port.unwrap_or(1965);
|
let port = config.port.unwrap_or(1965);
|
||||||
|
|
||||||
let listener = TcpListener::bind(format!("{}:{}", bind_host, port)).await?;
|
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 {
|
loop {
|
||||||
let (stream, _) = listener.accept().await?;
|
let (stream, _) = listener.accept().await?;
|
||||||
|
|
@ -186,14 +235,21 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// TLS connection with hostname routing
|
// TLS connection with hostname routing
|
||||||
match acceptor_clone.accept(stream).await {
|
match acceptor_clone.accept(stream).await {
|
||||||
Ok(tls_stream) => {
|
Ok(tls_stream) => {
|
||||||
if let Err(e) = server::handle_connection(tls_stream, &hosts_clone, max_concurrent, test_delay).await {
|
if let Err(e) = server::handle_connection(
|
||||||
eprintln!("Error handling connection: {}", e);
|
tls_stream,
|
||||||
|
&hosts_clone,
|
||||||
|
max_concurrent,
|
||||||
|
test_delay,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
tracing::error!("Error handling connection: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("TLS handshake failed: {}", e);
|
tracing::error!("TLS handshake failed: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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://") {
|
if let Some(url) = request.strip_prefix("gemini://") {
|
||||||
let host_port_end = url.find('/').unwrap_or(url.len());
|
let host_port_end = url.find('/').unwrap_or(url.len());
|
||||||
let host_port = &url[..host_port_end];
|
let host_port = &url[..host_port_end];
|
||||||
|
|
||||||
// Parse host and port
|
// Parse host and port
|
||||||
let (host, port_str) = if let Some(colon_pos) = host_port.find(':') {
|
let (host, port_str) = if let Some(colon_pos) = host_port.find(':') {
|
||||||
let host = &host_port[..colon_pos];
|
let host = &host_port[..colon_pos];
|
||||||
|
|
@ -20,21 +20,23 @@ pub fn parse_gemini_url(request: &str, hostname: &str, expected_port: u16) -> Re
|
||||||
} else {
|
} else {
|
||||||
(host_port, None)
|
(host_port, None)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Validate host
|
// Validate host
|
||||||
if host != hostname {
|
if host != hostname {
|
||||||
return Err(()); // Hostname mismatch
|
return Err(()); // Hostname mismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate port
|
// Validate port
|
||||||
let port = port_str
|
let port = port_str.and_then(|p| p.parse::<u16>().ok()).unwrap_or(1965);
|
||||||
.and_then(|p| p.parse::<u16>().ok())
|
|
||||||
.unwrap_or(1965);
|
|
||||||
if port != expected_port {
|
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())
|
Ok(path.trim().to_string())
|
||||||
} else {
|
} else {
|
||||||
Err(())
|
Err(())
|
||||||
|
|
@ -58,11 +60,11 @@ pub fn resolve_file_path(path: &str, dir: &str) -> Result<PathBuf, PathResolutio
|
||||||
} else {
|
} else {
|
||||||
Err(PathResolutionError::NotFound)
|
Err(PathResolutionError::NotFound)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
// Path validation failed - treat as not found
|
// Path validation failed - treat as not found
|
||||||
Err(PathResolutionError::NotFound)
|
Err(PathResolutionError::NotFound)
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,8 +92,18 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_gemini_url_valid() {
|
fn test_parse_gemini_url_valid() {
|
||||||
assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/", "gemini.jeena.net", 1965), Ok("/".to_string()));
|
assert_eq!(
|
||||||
assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/posts/test", "gemini.jeena.net", 1965), Ok("/posts/test".to_string()));
|
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]
|
#[test]
|
||||||
|
|
@ -130,14 +142,20 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_resolve_file_path_traversal() {
|
fn test_resolve_file_path_traversal() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
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]
|
#[test]
|
||||||
fn test_resolve_file_path_not_found() {
|
fn test_resolve_file_path_not_found() {
|
||||||
let temp_dir = TempDir::new().unwrap();
|
let temp_dir = TempDir::new().unwrap();
|
||||||
// Don't create the file, should return NotFound error
|
// 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]
|
#[test]
|
||||||
|
|
@ -163,4 +181,4 @@ mod tests {
|
||||||
let path = Path::new("test");
|
let path = Path::new("test");
|
||||||
assert_eq!(get_mime_type(path), "application/octet-stream");
|
assert_eq!(get_mime_type(path), "application/octet-stream");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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::fs;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -8,8 +8,6 @@ use tokio::net::TcpStream;
|
||||||
use tokio::time::{timeout, Duration};
|
use tokio::time::{timeout, Duration};
|
||||||
use tokio_rustls::server::TlsStream;
|
use tokio_rustls::server::TlsStream;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0);
|
static ACTIVE_REQUESTS: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
/// Extract hostname and path from a Gemini URL
|
/// 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
|
// URL decode the path
|
||||||
let decoded_path = urlencoding::decode(&path)
|
let decoded_path = urlencoding::decode(&path).map_err(|_| ())?;
|
||||||
.map_err(|_| ())?;
|
|
||||||
|
|
||||||
Ok((hostname.to_string(), decoded_path.to_string()))
|
Ok((hostname.to_string(), decoded_path.to_string()))
|
||||||
}
|
}
|
||||||
|
|
@ -54,7 +51,10 @@ pub async fn handle_connection(
|
||||||
let read_future = async {
|
let read_future = async {
|
||||||
loop {
|
loop {
|
||||||
if request_buf.len() >= MAX_REQUEST_SIZE {
|
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];
|
let mut byte = [0; 1];
|
||||||
stream.read_exact(&mut byte).await?;
|
stream.read_exact(&mut byte).await?;
|
||||||
|
|
@ -154,11 +154,7 @@ pub async fn handle_connection(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_file<S>(
|
async fn serve_file<S>(stream: &mut S, file_path: &Path, _request: &str) -> io::Result<()>
|
||||||
stream: &mut S,
|
|
||||||
file_path: &Path,
|
|
||||||
_request: &str,
|
|
||||||
) -> io::Result<()>
|
|
||||||
where
|
where
|
||||||
S: AsyncWriteExt + Unpin,
|
S: AsyncWriteExt + Unpin,
|
||||||
{
|
{
|
||||||
|
|
@ -177,14 +173,11 @@ where
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn send_response<S>(
|
async fn send_response<S>(stream: &mut S, response: &str) -> io::Result<()>
|
||||||
stream: &mut S,
|
|
||||||
response: &str,
|
|
||||||
) -> io::Result<()>
|
|
||||||
where
|
where
|
||||||
S: AsyncWriteExt + Unpin,
|
S: AsyncWriteExt + Unpin,
|
||||||
{
|
{
|
||||||
stream.write_all(response.as_bytes()).await?;
|
stream.write_all(response.as_bytes()).await?;
|
||||||
stream.flush().await?;
|
stream.flush().await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,8 @@ pub fn load_private_key(filename: &str) -> io::Result<rustls::PrivateKey> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(io::Error::new(io::ErrorKind::InvalidData, "No supported private key found"))
|
Err(io::Error::new(
|
||||||
}
|
io::ErrorKind::InvalidData,
|
||||||
|
"No supported private key found",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,22 @@ pub fn generate_test_certificates_for_host(temp_dir: &Path, hostname: &str) {
|
||||||
|
|
||||||
// Generate self-signed certificate for testing
|
// Generate self-signed certificate for testing
|
||||||
// This is a simplified version - in production, use proper certificates
|
// 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(
|
||||||
std::fs::write(&key_path, format!("-----BEGIN PRIVATE KEY-----\nTest key for {}\n-----END PRIVATE KEY-----\n", hostname)).unwrap();
|
&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;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
|
@ -42,12 +56,19 @@ fn generate_test_certificates(temp_dir: &Path) {
|
||||||
// Use openssl to generate a test certificate
|
// Use openssl to generate a test certificate
|
||||||
let output = Command::new("openssl")
|
let output = Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=localhost"
|
"-subj",
|
||||||
|
"/CN=localhost",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
|
@ -60,8 +81,3 @@ fn generate_test_certificates(temp_dir: &Path) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,87 +7,80 @@ fn test_missing_config_file() {
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg("nonexistent.toml")
|
.arg("nonexistent.toml")
|
||||||
|
.env("RUST_LOG", "error")
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
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("Config file 'nonexistent.toml' not found"));
|
||||||
assert!(stderr.contains("Create the config file with") || stderr.contains("Add at least one"));
|
assert!(stdout.contains("Create the config file with"));
|
||||||
}
|
|
||||||
|
|
||||||
#[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"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_nonexistent_root_directory() {
|
fn test_nonexistent_root_directory() {
|
||||||
let temp_dir = common::setup_test_environment();
|
let temp_dir = common::setup_test_environment();
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
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"
|
bind_host = "127.0.0.1"
|
||||||
|
|
||||||
["example.com"]
|
["example.com"]
|
||||||
root = "/definitely/does/not/exist"
|
root = "/definitely/does/not/exist"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg(&config_path)
|
.arg(&config_path)
|
||||||
|
.env("RUST_LOG", "error")
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
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("Failed to parse config file"));
|
||||||
assert!(stderr.contains("Create the directory and add your Gemini files (.gmi, .txt, images)"));
|
assert!(stderr.contains(
|
||||||
|
"Error for host 'example.com': Root directory '/definitely/does/not/exist' does not exist"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_missing_certificate_file() {
|
fn test_missing_certificate_file() {
|
||||||
let temp_dir = common::setup_test_environment();
|
let temp_dir = common::setup_test_environment();
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
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"
|
bind_host = "127.0.0.1"
|
||||||
|
|
||||||
["example.com"]
|
["example.com"]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "/nonexistent/cert.pem"
|
cert = "/nonexistent/cert.pem"
|
||||||
key = "{}"
|
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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg(&config_path)
|
.arg(&config_path)
|
||||||
|
.env("RUST_LOG", "error")
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
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"));
|
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 temp_dir = common::setup_test_environment();
|
||||||
let port = 1967 + (std::process::id() % 1000) as u16;
|
let port = 1967 + (std::process::id() % 1000) as u16;
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -104,7 +98,12 @@ port = {}
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let mut server_process = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
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));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
|
||||||
// Check server is still running (didn't exit with error)
|
// 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
|
// Kill server
|
||||||
server_process.kill().unwrap();
|
server_process.kill().unwrap();
|
||||||
|
|
@ -141,24 +143,38 @@ fn test_valid_multiple_hosts_startup() {
|
||||||
// Generate certificate for host1
|
// Generate certificate for host1
|
||||||
let cert_result1 = std::process::Command::new("openssl")
|
let cert_result1 = std::process::Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key1_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert1_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key1_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert1_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=host1.com"
|
"-subj",
|
||||||
|
"/CN=host1.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Generate certificate for host2
|
// Generate certificate for host2
|
||||||
let cert_result2 = std::process::Command::new("openssl")
|
let cert_result2 = std::process::Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key2_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert2_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key2_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert2_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=host2.com"
|
"-subj",
|
||||||
|
"/CN=host2.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
|
@ -167,7 +183,8 @@ fn test_valid_multiple_hosts_startup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -181,13 +198,14 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
temp_dir.path().join("host1").display(),
|
temp_dir.path().join("host1").display(),
|
||||||
cert1_path.display(),
|
cert1_path.display(),
|
||||||
key1_path.display(),
|
key1_path.display(),
|
||||||
temp_dir.path().join("host2").display(),
|
temp_dir.path().join("host2").display(),
|
||||||
cert2_path.display(),
|
cert2_path.display(),
|
||||||
key2_path.display());
|
key2_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
|
|
@ -201,7 +219,10 @@ key = "{}"
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
std::thread::sleep(std::time::Duration::from_millis(500));
|
||||||
|
|
||||||
// Check server is still running (didn't exit with error)
|
// 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
|
// Kill server
|
||||||
server_process.kill().unwrap();
|
server_process.kill().unwrap();
|
||||||
|
|
@ -222,12 +243,19 @@ fn test_multiple_hosts_missing_certificate() {
|
||||||
|
|
||||||
let cert_result = std::process::Command::new("openssl")
|
let cert_result = std::process::Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key1_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert1_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key1_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert1_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=host1.com"
|
"-subj",
|
||||||
|
"/CN=host1.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
|
@ -235,7 +263,8 @@ fn test_multiple_hosts_missing_certificate() {
|
||||||
panic!("Failed to generate test certificate");
|
panic!("Failed to generate test certificate");
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_content = format!(r#"
|
let config_content = format!(
|
||||||
|
r#"
|
||||||
bind_host = "127.0.0.1"
|
bind_host = "127.0.0.1"
|
||||||
|
|
||||||
["host1.com"]
|
["host1.com"]
|
||||||
|
|
@ -248,22 +277,26 @@ root = "{}"
|
||||||
cert = "/nonexistent/cert.pem"
|
cert = "/nonexistent/cert.pem"
|
||||||
key = "/nonexistent/key.pem"
|
key = "/nonexistent/key.pem"
|
||||||
"#,
|
"#,
|
||||||
temp_dir.path().join("host1").display(),
|
temp_dir.path().join("host1").display(),
|
||||||
cert1_path.display(),
|
cert1_path.display(),
|
||||||
key1_path.display(),
|
key1_path.display(),
|
||||||
temp_dir.path().join("host2").display());
|
temp_dir.path().join("host2").display()
|
||||||
|
);
|
||||||
|
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg(&config_path)
|
.arg(&config_path)
|
||||||
|
.env("RUST_LOG", "error")
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
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]
|
#[test]
|
||||||
|
|
@ -284,24 +317,38 @@ fn test_multiple_hosts_invalid_hostname() {
|
||||||
// Generate certificate for valid host
|
// Generate certificate for valid host
|
||||||
let cert_result1 = std::process::Command::new("openssl")
|
let cert_result1 = std::process::Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key1_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert1_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key1_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert1_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=valid.com"
|
"-subj",
|
||||||
|
"/CN=valid.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Generate certificate for invalid host (hostname validation happens before cert validation)
|
// Generate certificate for invalid host (hostname validation happens before cert validation)
|
||||||
let cert_result2 = std::process::Command::new("openssl")
|
let cert_result2 = std::process::Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key2_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert2_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key2_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert2_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=invalid.com"
|
"-subj",
|
||||||
|
"/CN=invalid.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
|
@ -309,7 +356,8 @@ fn test_multiple_hosts_invalid_hostname() {
|
||||||
panic!("Failed to generate test certificates");
|
panic!("Failed to generate test certificates");
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_content = format!(r#"
|
let config_content = format!(
|
||||||
|
r#"
|
||||||
bind_host = "127.0.0.1"
|
bind_host = "127.0.0.1"
|
||||||
|
|
||||||
["valid.com"]
|
["valid.com"]
|
||||||
|
|
@ -322,22 +370,24 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
temp_dir.path().join("validhost").display(),
|
temp_dir.path().join("validhost").display(),
|
||||||
cert1_path.display(),
|
cert1_path.display(),
|
||||||
key1_path.display(),
|
key1_path.display(),
|
||||||
temp_dir.path().join("invalidhost").display(),
|
temp_dir.path().join("invalidhost").display(),
|
||||||
cert2_path.display(),
|
cert2_path.display(),
|
||||||
key2_path.display());
|
key2_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let output = Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg(&config_path)
|
.arg(&config_path)
|
||||||
|
.env("RUST_LOG", "error")
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
||||||
assert!(stderr.contains("Invalid hostname 'bad..host.com'. Hostnames must be valid DNS names."));
|
assert!(stderr.contains("Invalid hostname 'bad..host.com'. Hostnames must be valid DNS names."));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ fn test_rate_limiting_with_concurrent_requests() {
|
||||||
let cert_path = temp_dir.path().join("cert.pem");
|
let cert_path = temp_dir.path().join("cert.pem");
|
||||||
let key_path = temp_dir.path().join("key.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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
max_concurrent_requests = 1
|
max_concurrent_requests = 1
|
||||||
|
|
@ -22,7 +23,12 @@ max_concurrent_requests = 1
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Start server binary with test delay to simulate processing time
|
// Start server binary with test delay to simulate processing time
|
||||||
|
|
@ -30,7 +36,7 @@ key = "{}"
|
||||||
.arg("--config")
|
.arg("--config")
|
||||||
.arg(&config_path)
|
.arg(&config_path)
|
||||||
.arg("--test-processing-delay")
|
.arg("--test-processing-delay")
|
||||||
.arg("3") // 3 second delay per request
|
.arg("3") // 3 second delay per request
|
||||||
.spawn()
|
.spawn()
|
||||||
.expect("Failed to start server");
|
.expect("Failed to start server");
|
||||||
|
|
||||||
|
|
@ -68,13 +74,30 @@ key = "{}"
|
||||||
let rate_limited_count = results.iter().filter(|r| r.starts_with("41")).count();
|
let rate_limited_count = results.iter().filter(|r| r.starts_with("41")).count();
|
||||||
|
|
||||||
// Debug output
|
// Debug output
|
||||||
println!("Results: {:?}", results);
|
tracing::debug!("Test results: {:?}", results);
|
||||||
println!("Success: {}, Rate limited: {}", success_count, rate_limited_count);
|
tracing::debug!(
|
||||||
|
"Success: {}, Rate limited: {}",
|
||||||
|
success_count,
|
||||||
|
rate_limited_count
|
||||||
|
);
|
||||||
|
|
||||||
// Strict validation - rate limiting must work deterministically with delay
|
// 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!(
|
||||||
assert_eq!(rate_limited_count, 4, "Expected exactly 4 rate limited requests with limit=1, got {}. Results: {:?}", rate_limited_count, results);
|
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
|
// Verify all requests received valid responses
|
||||||
assert_eq!(success_count + rate_limited_count, 5, "All 5 requests should receive responses. Results: {:?}", results);
|
assert_eq!(
|
||||||
}
|
success_count + rate_limited_count,
|
||||||
|
5,
|
||||||
|
"All 5 requests should receive responses. Results: {:?}",
|
||||||
|
results
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,12 +17,19 @@ fn test_single_host_config() {
|
||||||
|
|
||||||
let cert_result = Command::new("openssl")
|
let cert_result = Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &key_path.to_string_lossy(),
|
"-x509",
|
||||||
"-out", &cert_path.to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"rsa:2048",
|
||||||
|
"-keyout",
|
||||||
|
&key_path.to_string_lossy(),
|
||||||
|
"-out",
|
||||||
|
&cert_path.to_string_lossy(),
|
||||||
|
"-days",
|
||||||
|
"1",
|
||||||
"-nodes",
|
"-nodes",
|
||||||
"-subj", "/CN=example.com"
|
"-subj",
|
||||||
|
"/CN=example.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
|
@ -30,7 +37,8 @@ fn test_single_host_config() {
|
||||||
panic!("Failed to generate test certificates for config test");
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -38,7 +46,12 @@ port = {}
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
|
|
@ -48,7 +61,10 @@ key = "{}"
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,7 +72,8 @@ key = "{}"
|
||||||
fn test_multiple_hosts_config() {
|
fn test_multiple_hosts_config() {
|
||||||
let temp_dir = common::setup_test_environment();
|
let temp_dir = common::setup_test_environment();
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let config_content = format!(r#"
|
let config_content = format!(
|
||||||
|
r#"
|
||||||
[site1.com]
|
[site1.com]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
|
|
@ -69,12 +86,14 @@ key = "{}"
|
||||||
|
|
||||||
bind_host = "127.0.0.1"
|
bind_host = "127.0.0.1"
|
||||||
port = 1965
|
port = 1965
|
||||||
"#, temp_dir.path().join("site1").display(),
|
"#,
|
||||||
temp_dir.path().join("site1_cert.pem").display(),
|
temp_dir.path().join("site1").display(),
|
||||||
temp_dir.path().join("site1_key.pem").display(),
|
temp_dir.path().join("site1_cert.pem").display(),
|
||||||
temp_dir.path().join("site2").display(),
|
temp_dir.path().join("site1_key.pem").display(),
|
||||||
temp_dir.path().join("site2_cert.pem").display(),
|
temp_dir.path().join("site2").display(),
|
||||||
temp_dir.path().join("site2_key.pem").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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Create additional directories and generate certificates
|
// Create additional directories and generate certificates
|
||||||
|
|
@ -87,24 +106,38 @@ port = 1965
|
||||||
// Site 1 certificate
|
// Site 1 certificate
|
||||||
let cert_result1 = Command::new("openssl")
|
let cert_result1 = Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &temp_dir.path().join("site1_key.pem").to_string_lossy(),
|
"-x509",
|
||||||
"-out", &temp_dir.path().join("site1_cert.pem").to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"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",
|
"-nodes",
|
||||||
"-subj", "/CN=site1.com"
|
"-subj",
|
||||||
|
"/CN=site1.com",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Site 2 certificate
|
// Site 2 certificate
|
||||||
let cert_result2 = Command::new("openssl")
|
let cert_result2 = Command::new("openssl")
|
||||||
.args(&[
|
.args(&[
|
||||||
"req", "-x509", "-newkey", "rsa:2048",
|
"req",
|
||||||
"-keyout", &temp_dir.path().join("site2_key.pem").to_string_lossy(),
|
"-x509",
|
||||||
"-out", &temp_dir.path().join("site2_cert.pem").to_string_lossy(),
|
"-newkey",
|
||||||
"-days", "1",
|
"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",
|
"-nodes",
|
||||||
"-subj", "/CN=site2.org"
|
"-subj",
|
||||||
|
"/CN=site2.org",
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
|
@ -114,7 +147,8 @@ port = 1965
|
||||||
|
|
||||||
// Test server starts successfully with multiple host config
|
// Test server starts successfully with multiple host config
|
||||||
let port = 1968 + (std::process::id() % 1000) as u16;
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -128,13 +162,14 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
temp_dir.path().join("site1").display(),
|
temp_dir.path().join("site1").display(),
|
||||||
temp_dir.path().join("site1_cert.pem").display(),
|
temp_dir.path().join("site1_cert.pem").display(),
|
||||||
temp_dir.path().join("site1_key.pem").display(),
|
temp_dir.path().join("site1_key.pem").display(),
|
||||||
temp_dir.path().join("site2").display(),
|
temp_dir.path().join("site2").display(),
|
||||||
temp_dir.path().join("site2_cert.pem").display(),
|
temp_dir.path().join("site2_cert.pem").display(),
|
||||||
temp_dir.path().join("site2_key.pem").display());
|
temp_dir.path().join("site2_key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
|
|
@ -144,7 +179,10 @@ key = "{}"
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -177,14 +215,17 @@ root = "/tmp/content"
|
||||||
fn test_invalid_hostname_config() {
|
fn test_invalid_hostname_config() {
|
||||||
let temp_dir = common::setup_test_environment();
|
let temp_dir = common::setup_test_environment();
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let config_content = format!(r#"
|
let config_content = format!(
|
||||||
|
r#"
|
||||||
["invalid"]
|
["invalid"]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#, temp_dir.path().join("content").display(),
|
"#,
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("content").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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let output = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let output = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
|
|
@ -224,7 +265,8 @@ port = 1965
|
||||||
fn test_duplicate_hostname_config() {
|
fn test_duplicate_hostname_config() {
|
||||||
let temp_dir = common::setup_test_environment();
|
let temp_dir = common::setup_test_environment();
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let config_content = format!(r#"
|
let config_content = format!(
|
||||||
|
r#"
|
||||||
[example.com]
|
[example.com]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
|
|
@ -234,12 +276,14 @@ key = "{}"
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#, temp_dir.path().join("path1").display(),
|
"#,
|
||||||
temp_dir.path().join("cert1.pem").display(),
|
temp_dir.path().join("path1").display(),
|
||||||
temp_dir.path().join("key1.pem").display(),
|
temp_dir.path().join("cert1.pem").display(),
|
||||||
temp_dir.path().join("path2").display(),
|
temp_dir.path().join("key1.pem").display(),
|
||||||
temp_dir.path().join("cert2.pem").display(),
|
temp_dir.path().join("path2").display(),
|
||||||
temp_dir.path().join("key2.pem").display());
|
temp_dir.path().join("cert2.pem").display(),
|
||||||
|
temp_dir.path().join("key2.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Create the directories and certs
|
// Create the directories and certs
|
||||||
|
|
@ -266,7 +310,8 @@ fn test_host_with_port_override() {
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
// Test server starts successfully
|
// Test server starts successfully
|
||||||
let port = 1969 + (std::process::id() % 1000) as u16;
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -275,10 +320,12 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
port = 1970 # Override global port
|
port = 1970 # Override global port
|
||||||
"#, port,
|
"#,
|
||||||
temp_dir.path().join("content").display(),
|
port,
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("content").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();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||||
|
|
@ -288,7 +335,10 @@ port = 1970 # Override global port
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(500));
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,4 +353,4 @@ fn test_config_file_not_found() {
|
||||||
assert!(!output.status.success());
|
assert!(!output.status.success());
|
||||||
let stderr = String::from_utf8(output.stderr).unwrap();
|
let stderr = String::from_utf8(output.stderr).unwrap();
|
||||||
assert!(stderr.contains("Config file 'nonexistent.toml' not found"));
|
assert!(stderr.contains("Config file 'nonexistent.toml' not found"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ fn test_concurrent_requests_multiple_hosts() {
|
||||||
for host in &hosts {
|
for host in &hosts {
|
||||||
let root_dir = temp_dir.path().join(host.replace(".", "_"));
|
let root_dir = temp_dir.path().join(host.replace(".", "_"));
|
||||||
std::fs::create_dir(&root_dir).unwrap();
|
std::fs::create_dir(&root_dir).unwrap();
|
||||||
std::fs::write(
|
std::fs::write(root_dir.join("index.gmi"), format!("Welcome to {}", host)).unwrap();
|
||||||
root_dir.join("index.gmi"),
|
|
||||||
format!("Welcome to {}", host),
|
|
||||||
).unwrap();
|
|
||||||
host_roots.push(root_dir);
|
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 config_path = temp_dir.path().join("config.toml");
|
||||||
let port = 1969 + (std::process::id() % 1000) as u16;
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
"#, port);
|
"#,
|
||||||
|
port
|
||||||
|
);
|
||||||
|
|
||||||
for (i, host) in hosts.iter().enumerate() {
|
for (i, host) in hosts.iter().enumerate() {
|
||||||
config_content.push_str(&format!(r#"
|
config_content.push_str(&format!(
|
||||||
|
r#"
|
||||||
["{}"]
|
["{}"]
|
||||||
root = "{}"
|
root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
host,
|
host,
|
||||||
host_roots[i].display(),
|
host_roots[i].display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display()));
|
temp_dir.path().join("key.pem").display()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
@ -65,9 +67,20 @@ key = "{}"
|
||||||
let port_clone = Arc::clone(&port_arc);
|
let port_clone = Arc::clone(&port_arc);
|
||||||
|
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
let response = make_gemini_request("127.0.0.1", *port_clone, &format!("gemini://{}/", host));
|
let response =
|
||||||
assert!(response.starts_with("20"), "Request {} failed: {}", i, response);
|
make_gemini_request("127.0.0.1", *port_clone, &format!("gemini://{}/", host));
|
||||||
assert!(response.contains(&format!("Welcome to {}", host)), "Wrong content for request {}: {}", i, response);
|
assert!(
|
||||||
|
response.starts_with("20"),
|
||||||
|
"Request {} failed: {}",
|
||||||
|
i,
|
||||||
|
response
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
response.contains(&format!("Welcome to {}", host)),
|
||||||
|
"Wrong content for request {}: {}",
|
||||||
|
i,
|
||||||
|
response
|
||||||
|
);
|
||||||
response
|
response
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -97,7 +110,8 @@ fn test_mixed_valid_invalid_hostnames() {
|
||||||
// Create config
|
// Create config
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let port = 1970 + (std::process::id() % 1000) as u16;
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -106,10 +120,11 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
root_dir.display(),
|
root_dir.display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display());
|
temp_dir.path().join("key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Start server with TLS
|
// Start server with TLS
|
||||||
|
|
@ -123,8 +138,16 @@ key = "{}"
|
||||||
|
|
||||||
// Test valid hostname
|
// Test valid hostname
|
||||||
let valid_response = make_gemini_request("127.0.0.1", port, "gemini://valid.com/");
|
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!(
|
||||||
assert!(valid_response.contains("Valid site content"), "Should serve correct content: {}", valid_response);
|
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
|
// Test various invalid hostnames
|
||||||
let invalid_hosts = vec![
|
let invalid_hosts = vec![
|
||||||
|
|
@ -135,8 +158,14 @@ key = "{}"
|
||||||
];
|
];
|
||||||
|
|
||||||
for invalid_host in invalid_hosts {
|
for invalid_host in invalid_hosts {
|
||||||
let response = make_gemini_request("127.0.0.1", port, &format!("gemini://{}/", invalid_host));
|
let response =
|
||||||
assert!(response.starts_with("53"), "Invalid host '{}' should return 53, got: {}", invalid_host, 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();
|
server_process.kill().unwrap();
|
||||||
|
|
@ -153,7 +182,8 @@ fn test_load_performance_basic() {
|
||||||
|
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let port = 1971 + (std::process::id() % 1000) as u16;
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -162,10 +192,11 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
root_dir.display(),
|
root_dir.display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display());
|
temp_dir.path().join("key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Start server with TLS
|
// Start server with TLS
|
||||||
|
|
@ -183,17 +214,30 @@ key = "{}"
|
||||||
|
|
||||||
for i in 0..NUM_REQUESTS {
|
for i in 0..NUM_REQUESTS {
|
||||||
let response = make_gemini_request("127.0.0.1", port, "gemini://perf.com/");
|
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 elapsed = start.elapsed();
|
||||||
let avg_time = elapsed.as_millis() as f64 / NUM_REQUESTS as f64;
|
let avg_time = elapsed.as_millis() as f64 / NUM_REQUESTS as f64;
|
||||||
|
|
||||||
println!("Processed {} requests in {:?} (avg: {:.2}ms per request)",
|
tracing::debug!(
|
||||||
NUM_REQUESTS, elapsed, avg_time);
|
"Processed {} requests in {:?} (avg: {:.2}ms per request)",
|
||||||
|
NUM_REQUESTS,
|
||||||
|
elapsed,
|
||||||
|
avg_time
|
||||||
|
);
|
||||||
|
|
||||||
// Basic performance check - should be reasonably fast
|
// 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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -222,7 +266,8 @@ fn test_full_request_lifecycle() {
|
||||||
let cert_path = temp_dir.path().join("cert.pem");
|
let cert_path = temp_dir.path().join("cert.pem");
|
||||||
let key_path = temp_dir.path().join("key.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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -231,10 +276,11 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
root_dir.display(),
|
root_dir.display(),
|
||||||
cert_path.display(),
|
cert_path.display(),
|
||||||
key_path.display());
|
key_path.display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Start server with TLS
|
// Start server with TLS
|
||||||
|
|
@ -248,27 +294,64 @@ key = "{}"
|
||||||
|
|
||||||
// Test root index
|
// Test root index
|
||||||
let root_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/");
|
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!(
|
||||||
assert!(root_response.contains("Main site content"), "Wrong root content: {}", root_response);
|
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
|
// Test explicit index
|
||||||
let index_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/index.gmi");
|
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!(
|
||||||
assert!(index_response.contains("Main site content"), "Wrong index content: {}", index_response);
|
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
|
// Test subdirectory index
|
||||||
let blog_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/blog/");
|
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!(
|
||||||
assert!(blog_response.contains("Blog index content"), "Wrong blog content: {}", blog_response);
|
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
|
// Test individual file
|
||||||
let about_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/about.gmi");
|
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!(
|
||||||
assert!(about_response.contains("About page content"), "Wrong about content: {}", about_response);
|
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
|
// Test not found
|
||||||
let notfound_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/nonexistent.gmi");
|
let notfound_response =
|
||||||
assert!(notfound_response.starts_with("51"), "Not found should return 51: {}", 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();
|
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),
|
Err(e) => format!("Error: Failed to run Python client: {}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
mod common;
|
mod common;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_per_host_content_isolation() {
|
fn test_per_host_content_isolation() {
|
||||||
let temp_dir = common::setup_test_environment();
|
let temp_dir = common::setup_test_environment();
|
||||||
|
|
@ -19,7 +17,8 @@ fn test_per_host_content_isolation() {
|
||||||
// Create config with two hosts
|
// Create config with two hosts
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let port = 1965 + (std::process::id() % 1000) as u16; // Use dynamic port
|
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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -33,13 +32,14 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
site1_root.display(),
|
site1_root.display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display(),
|
temp_dir.path().join("key.pem").display(),
|
||||||
site2_root.display(),
|
site2_root.display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display());
|
temp_dir.path().join("key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
// Start server with TLS
|
// Start server with TLS
|
||||||
|
|
@ -54,13 +54,29 @@ key = "{}"
|
||||||
|
|
||||||
// Test site1.com serves its content
|
// Test site1.com serves its content
|
||||||
let response1 = make_gemini_request("127.0.0.1", port, "gemini://site1.com/");
|
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!(
|
||||||
assert!(response1.contains("Welcome to Site 1"), "Should serve site1 content, got: {}", response1);
|
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
|
// Test site2.org serves its content
|
||||||
let response2 = make_gemini_request("127.0.0.1", port, "gemini://site2.org/");
|
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!(
|
||||||
assert!(response2.contains("Welcome to Site 2"), "Should serve site2 content, got: {}", response2);
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -73,7 +89,11 @@ fn test_per_host_path_security() {
|
||||||
let site1_root = temp_dir.path().join("site1");
|
let site1_root = temp_dir.path().join("site1");
|
||||||
std::fs::create_dir(&site1_root).unwrap();
|
std::fs::create_dir(&site1_root).unwrap();
|
||||||
std::fs::create_dir(site1_root.join("subdir")).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
|
// Create config
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
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 cert_path = temp_dir.path().join("cert.pem");
|
||||||
let key_path = temp_dir.path().join("key.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"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -90,10 +111,11 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
site1_root.display(),
|
site1_root.display(),
|
||||||
cert_path.display(),
|
cert_path.display(),
|
||||||
key_path.display());
|
key_path.display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, config_content).unwrap();
|
std::fs::write(&config_path, config_content).unwrap();
|
||||||
|
|
||||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
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
|
// Test path traversal attempt should be blocked
|
||||||
let response = make_gemini_request("127.0.0.1", port, "gemini://site1.com/../../../etc/passwd");
|
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
|
// Test valid subdirectory access should work
|
||||||
let response = make_gemini_request("127.0.0.1", port, "gemini://site1.com/subdir/secret.gmi");
|
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!(
|
||||||
assert!(response.contains("Secret content"), "Should serve content from subdirectory, got: {}", response);
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -128,4 +162,4 @@ fn make_gemini_request(host: &str, port: u16, url: &str) -> String {
|
||||||
.output()
|
.output()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
String::from_utf8(output.stdout).unwrap()
|
String::from_utf8(output.stdout).unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,8 @@ fn test_virtual_host_routing_multiple_hosts() {
|
||||||
|
|
||||||
// Create config with two hosts
|
// Create config with two hosts
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let content = format!(r#"
|
let content = format!(
|
||||||
|
r#"
|
||||||
bind_host = "127.0.0.1"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -85,20 +86,29 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
temp_dir.path().join("site1").display(),
|
temp_dir.path().join("site1").display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display(),
|
temp_dir.path().join("key.pem").display(),
|
||||||
temp_dir.path().join("site2").display(),
|
temp_dir.path().join("site2").display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display());
|
temp_dir.path().join("key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, content).unwrap();
|
std::fs::write(&config_path, content).unwrap();
|
||||||
|
|
||||||
// Create host-specific content
|
// Create host-specific content
|
||||||
std::fs::create_dir_all(temp_dir.path().join("site1")).unwrap();
|
std::fs::create_dir_all(temp_dir.path().join("site1")).unwrap();
|
||||||
std::fs::create_dir_all(temp_dir.path().join("site2")).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(
|
||||||
std::fs::write(temp_dir.path().join("site2").join("index.gmi"), "# Site 2 Content\n").unwrap();
|
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)
|
// Use the same certs for both hosts (server uses first cert anyway)
|
||||||
|
|
||||||
|
|
@ -114,11 +124,19 @@ key = "{}"
|
||||||
|
|
||||||
// Test request to site1.com with TLS
|
// Test request to site1.com with TLS
|
||||||
let response1 = make_gemini_request("127.0.0.1", port, "gemini://site1.com/index.gmi");
|
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
|
// Test request to site2.org
|
||||||
let response2 = make_gemini_request("127.0.0.1", port, "gemini://site2.org/index.gmi");
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -132,7 +150,8 @@ fn test_virtual_host_routing_known_hostname() {
|
||||||
|
|
||||||
// Config with only one host
|
// Config with only one host
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let content = format!(r#"
|
let content = format!(
|
||||||
|
r#"
|
||||||
bind_host = "127.0.0.1"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -141,10 +160,11 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
temp_dir.path().join("content").display(),
|
temp_dir.path().join("content").display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display());
|
temp_dir.path().join("key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, content).unwrap();
|
std::fs::write(&config_path, content).unwrap();
|
||||||
|
|
||||||
// Start server with TLS
|
// Start server with TLS
|
||||||
|
|
@ -159,7 +179,11 @@ key = "{}"
|
||||||
|
|
||||||
// Test request to unknown hostname
|
// Test request to unknown hostname
|
||||||
let response = make_gemini_request("127.0.0.1", port, "gemini://unknown.com/index.gmi");
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
@ -173,7 +197,8 @@ fn test_virtual_host_routing_malformed_url() {
|
||||||
|
|
||||||
// Config with one host
|
// Config with one host
|
||||||
let config_path = temp_dir.path().join("config.toml");
|
let config_path = temp_dir.path().join("config.toml");
|
||||||
let content = format!(r#"
|
let content = format!(
|
||||||
|
r#"
|
||||||
bind_host = "127.0.0.1"
|
bind_host = "127.0.0.1"
|
||||||
port = {}
|
port = {}
|
||||||
|
|
||||||
|
|
@ -182,10 +207,11 @@ root = "{}"
|
||||||
cert = "{}"
|
cert = "{}"
|
||||||
key = "{}"
|
key = "{}"
|
||||||
"#,
|
"#,
|
||||||
port,
|
port,
|
||||||
temp_dir.path().join("content").display(),
|
temp_dir.path().join("content").display(),
|
||||||
temp_dir.path().join("cert.pem").display(),
|
temp_dir.path().join("cert.pem").display(),
|
||||||
temp_dir.path().join("key.pem").display());
|
temp_dir.path().join("key.pem").display()
|
||||||
|
);
|
||||||
std::fs::write(&config_path, content).unwrap();
|
std::fs::write(&config_path, content).unwrap();
|
||||||
|
|
||||||
// Start server with TLS
|
// Start server with TLS
|
||||||
|
|
@ -200,7 +226,11 @@ key = "{}"
|
||||||
|
|
||||||
// Test malformed URL (wrong protocol)
|
// Test malformed URL (wrong protocol)
|
||||||
let response = make_gemini_request("127.0.0.1", port, "http://example.com/index.gmi");
|
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();
|
server_process.kill().unwrap();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue