feat: Implement virtual hosting for multi-domain Gemini server
- Add hostname-based request routing for multiple capsules per server - Parse virtual host configs from TOML sections ([hostname]) - Implement per-host certificate and content isolation - Add comprehensive virtual host testing and validation - Update docs and examples for multi-host deployments This enables Pollux to serve multiple Gemini domains from one instance, providing the foundation for multi-tenant Gemini hosting.
This commit is contained in:
parent
c193d831ed
commit
0459cb6220
22 changed files with 2296 additions and 406 deletions
298
tests/virtual_host_integration.rs
Normal file
298
tests/virtual_host_integration.rs
Normal file
|
|
@ -0,0 +1,298 @@
|
|||
mod common;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_requests_multiple_hosts() {
|
||||
let temp_dir = common::setup_test_environment();
|
||||
|
||||
// Create content for multiple hosts
|
||||
let hosts = vec!["site1.com", "site2.org", "site3.net"];
|
||||
let mut host_roots = Vec::new();
|
||||
|
||||
for host in &hosts {
|
||||
let root_dir = temp_dir.path().join(host.replace(".", "_"));
|
||||
std::fs::create_dir(&root_dir).unwrap();
|
||||
std::fs::write(
|
||||
root_dir.join("index.gmi"),
|
||||
format!("Welcome to {}", host),
|
||||
).unwrap();
|
||||
host_roots.push(root_dir);
|
||||
}
|
||||
|
||||
// Create config with multiple hosts
|
||||
let config_path = temp_dir.path().join("config.toml");
|
||||
let port = 1969 + (std::process::id() % 1000) as u16;
|
||||
|
||||
let mut config_content = format!(r#"
|
||||
bind_host = "127.0.0.1"
|
||||
port = {}
|
||||
|
||||
"#, port);
|
||||
|
||||
for (i, host) in hosts.iter().enumerate() {
|
||||
config_content.push_str(&format!(r#"
|
||||
["{}"]
|
||||
root = "{}"
|
||||
cert = "{}"
|
||||
key = "{}"
|
||||
"#,
|
||||
host,
|
||||
host_roots[i].display(),
|
||||
temp_dir.path().join("cert.pem").display(),
|
||||
temp_dir.path().join("key.pem").display()));
|
||||
}
|
||||
|
||||
std::fs::write(&config_path, config_content).unwrap();
|
||||
|
||||
// Start server with TLS
|
||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||
.arg("--config")
|
||||
.arg(&config_path)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
// Spawn multiple threads making concurrent requests
|
||||
let mut handles = Vec::new();
|
||||
let port_arc = Arc::new(port);
|
||||
|
||||
for i in 0..10 {
|
||||
let host = hosts[i % hosts.len()].to_string();
|
||||
let port_clone = Arc::clone(&port_arc);
|
||||
|
||||
let handle = thread::spawn(move || {
|
||||
let response = make_gemini_request("127.0.0.1", *port_clone, &format!("gemini://{}/", host));
|
||||
assert!(response.starts_with("20"), "Request {} failed: {}", i, response);
|
||||
assert!(response.contains(&format!("Welcome to {}", host)), "Wrong content for request {}: {}", i, response);
|
||||
response
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Collect results
|
||||
let mut results = Vec::new();
|
||||
for handle in handles {
|
||||
results.push(handle.join().unwrap());
|
||||
}
|
||||
|
||||
assert_eq!(results.len(), 10, "All concurrent requests should complete");
|
||||
|
||||
server_process.kill().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_valid_invalid_hostnames() {
|
||||
let temp_dir = common::setup_test_environment();
|
||||
|
||||
// Create content for one valid host
|
||||
let root_dir = temp_dir.path().join("valid_site");
|
||||
std::fs::create_dir(&root_dir).unwrap();
|
||||
std::fs::write(root_dir.join("index.gmi"), "Valid site content").unwrap();
|
||||
|
||||
// Create config
|
||||
let config_path = temp_dir.path().join("config.toml");
|
||||
let port = 1970 + (std::process::id() % 1000) as u16;
|
||||
let config_content = format!(r#"
|
||||
bind_host = "127.0.0.1"
|
||||
port = {}
|
||||
|
||||
["valid.com"]
|
||||
root = "{}"
|
||||
cert = "{}"
|
||||
key = "{}"
|
||||
"#,
|
||||
port,
|
||||
root_dir.display(),
|
||||
temp_dir.path().join("cert.pem").display(),
|
||||
temp_dir.path().join("key.pem").display());
|
||||
std::fs::write(&config_path, config_content).unwrap();
|
||||
|
||||
// Start server with TLS
|
||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||
.arg("--config")
|
||||
.arg(&config_path)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
// Test valid hostname
|
||||
let valid_response = make_gemini_request("127.0.0.1", port, "gemini://valid.com/");
|
||||
assert!(valid_response.starts_with("20"), "Valid host should work: {}", valid_response);
|
||||
assert!(valid_response.contains("Valid site content"), "Should serve correct content: {}", valid_response);
|
||||
|
||||
// Test various invalid hostnames
|
||||
let invalid_hosts = vec![
|
||||
"invalid.com",
|
||||
"unknown.net",
|
||||
"nonexistent.invalid",
|
||||
"site.with.dots.com",
|
||||
];
|
||||
|
||||
for invalid_host in invalid_hosts {
|
||||
let response = make_gemini_request("127.0.0.1", port, &format!("gemini://{}/", invalid_host));
|
||||
assert!(response.starts_with("53"), "Invalid host '{}' should return 53, got: {}", invalid_host, response);
|
||||
}
|
||||
|
||||
server_process.kill().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_performance_basic() {
|
||||
let temp_dir = common::setup_test_environment();
|
||||
|
||||
// Create a simple host
|
||||
let root_dir = temp_dir.path().join("perf_test");
|
||||
std::fs::create_dir(&root_dir).unwrap();
|
||||
std::fs::write(root_dir.join("index.gmi"), "Performance test content").unwrap();
|
||||
|
||||
let config_path = temp_dir.path().join("config.toml");
|
||||
let port = 1971 + (std::process::id() % 1000) as u16;
|
||||
let config_content = format!(r#"
|
||||
bind_host = "127.0.0.1"
|
||||
port = {}
|
||||
|
||||
["perf.com"]
|
||||
root = "{}"
|
||||
cert = "{}"
|
||||
key = "{}"
|
||||
"#,
|
||||
port,
|
||||
root_dir.display(),
|
||||
temp_dir.path().join("cert.pem").display(),
|
||||
temp_dir.path().join("key.pem").display());
|
||||
std::fs::write(&config_path, config_content).unwrap();
|
||||
|
||||
// Start server with TLS
|
||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||
.arg("--config")
|
||||
.arg(&config_path)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
// Measure time for multiple requests
|
||||
let start = Instant::now();
|
||||
const NUM_REQUESTS: usize = 50;
|
||||
|
||||
for i in 0..NUM_REQUESTS {
|
||||
let response = make_gemini_request("127.0.0.1", port, "gemini://perf.com/");
|
||||
assert!(response.starts_with("20"), "Request {} failed: {}", i, response);
|
||||
}
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
let avg_time = elapsed.as_millis() as f64 / NUM_REQUESTS as f64;
|
||||
|
||||
println!("Processed {} requests in {:?} (avg: {:.2}ms per request)",
|
||||
NUM_REQUESTS, elapsed, avg_time);
|
||||
|
||||
// Basic performance check - should be reasonably fast
|
||||
assert!(avg_time < 100.0, "Average request time too slow: {:.2}ms", avg_time);
|
||||
|
||||
server_process.kill().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_request_lifecycle() {
|
||||
let temp_dir = common::setup_test_environment();
|
||||
|
||||
// Create complex content structure
|
||||
let root_dir = temp_dir.path().join("lifecycle_test");
|
||||
std::fs::create_dir(&root_dir).unwrap();
|
||||
|
||||
// Create directory with index
|
||||
let blog_dir = root_dir.join("blog");
|
||||
std::fs::create_dir(&blog_dir).unwrap();
|
||||
std::fs::write(blog_dir.join("index.gmi"), "Blog index content").unwrap();
|
||||
|
||||
// Create individual file
|
||||
std::fs::write(root_dir.join("about.gmi"), "About page content").unwrap();
|
||||
|
||||
// Create root index
|
||||
std::fs::write(root_dir.join("index.gmi"), "Main site content").unwrap();
|
||||
|
||||
let config_path = temp_dir.path().join("config.toml");
|
||||
let port = 1972 + (std::process::id() % 1000) as u16;
|
||||
let cert_path = temp_dir.path().join("cert.pem");
|
||||
let key_path = temp_dir.path().join("key.pem");
|
||||
|
||||
let config_content = format!(r#"
|
||||
bind_host = "127.0.0.1"
|
||||
port = {}
|
||||
|
||||
["lifecycle.com"]
|
||||
root = "{}"
|
||||
cert = "{}"
|
||||
key = "{}"
|
||||
"#,
|
||||
port,
|
||||
root_dir.display(),
|
||||
cert_path.display(),
|
||||
key_path.display());
|
||||
std::fs::write(&config_path, config_content).unwrap();
|
||||
|
||||
// Start server with TLS
|
||||
let mut server_process = std::process::Command::new(env!("CARGO_BIN_EXE_pollux"))
|
||||
.arg("--config")
|
||||
.arg(&config_path)
|
||||
.spawn()
|
||||
.unwrap();
|
||||
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
// Test root index
|
||||
let root_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/");
|
||||
assert!(root_response.starts_with("20"), "Root request failed: {}", root_response);
|
||||
assert!(root_response.contains("Main site content"), "Wrong root content: {}", root_response);
|
||||
|
||||
// Test explicit index
|
||||
let index_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/index.gmi");
|
||||
assert!(index_response.starts_with("20"), "Index request failed: {}", index_response);
|
||||
assert!(index_response.contains("Main site content"), "Wrong index content: {}", index_response);
|
||||
|
||||
// Test subdirectory index
|
||||
let blog_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/blog/");
|
||||
assert!(blog_response.starts_with("20"), "Blog request failed: {}", blog_response);
|
||||
assert!(blog_response.contains("Blog index content"), "Wrong blog content: {}", blog_response);
|
||||
|
||||
// Test individual file
|
||||
let about_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/about.gmi");
|
||||
assert!(about_response.starts_with("20"), "About request failed: {}", about_response);
|
||||
assert!(about_response.contains("About page content"), "Wrong about content: {}", about_response);
|
||||
|
||||
// Test not found
|
||||
let notfound_response = make_gemini_request("127.0.0.1", port, "gemini://lifecycle.com/nonexistent.gmi");
|
||||
assert!(notfound_response.starts_with("51"), "Not found should return 51: {}", notfound_response);
|
||||
|
||||
server_process.kill().unwrap();
|
||||
}
|
||||
|
||||
fn make_gemini_request(host: &str, port: u16, url: &str) -> String {
|
||||
// Use the Python client for TLS requests
|
||||
use std::process::Command;
|
||||
|
||||
let output = Command::new("python3")
|
||||
.arg("tests/gemini_test_client.py")
|
||||
.arg(url)
|
||||
.env("GEMINI_PORT", &port.to_string())
|
||||
.env("GEMINI_CONNECT_HOST", host)
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(result) => {
|
||||
if result.status.success() {
|
||||
String::from_utf8_lossy(&result.stdout).trim().to_string()
|
||||
} else {
|
||||
format!("Error: Python client failed with status {}", result.status)
|
||||
}
|
||||
}
|
||||
Err(e) => format!("Error: Failed to run Python client: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue