Implement BACKLOG.md items: config-only, request limits, URL validation
- Remove CLI options except --config and --test-processing-delay - Enforce 1026 byte request limit per Gemini spec (1024 + 2 for CRLF) - Add comprehensive URL parsing with host and port validation - Reject malformed URIs and wrong ports with 59 Bad Request - Update tests for new URL parsing signature - Fix clippy warning in port parsing
This commit is contained in:
parent
6a61b562f5
commit
f05b9373f1
3 changed files with 38 additions and 37 deletions
34
src/main.rs
34
src/main.rs
|
|
@ -34,26 +34,6 @@ struct Args {
|
||||||
#[arg(short = 'C', long)]
|
#[arg(short = 'C', long)]
|
||||||
config: Option<String>,
|
config: Option<String>,
|
||||||
|
|
||||||
/// Directory to serve files from
|
|
||||||
#[arg(short, long)]
|
|
||||||
root: Option<String>,
|
|
||||||
|
|
||||||
/// Path to certificate file
|
|
||||||
#[arg(short, long)]
|
|
||||||
cert: Option<String>,
|
|
||||||
|
|
||||||
/// Path to private key file
|
|
||||||
#[arg(short, long)]
|
|
||||||
key: Option<String>,
|
|
||||||
|
|
||||||
/// Port to listen on
|
|
||||||
#[arg(short, long)]
|
|
||||||
port: Option<u16>,
|
|
||||||
|
|
||||||
/// Hostname for the server
|
|
||||||
#[arg(short = 'H', long)]
|
|
||||||
host: Option<String>,
|
|
||||||
|
|
||||||
/// TESTING ONLY: Add delay before processing (seconds) [debug builds only]
|
/// TESTING ONLY: Add delay before processing (seconds) [debug builds only]
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
#[arg(long, value_name = "SECONDS")]
|
#[arg(long, value_name = "SECONDS")]
|
||||||
|
|
@ -82,12 +62,12 @@ async fn main() {
|
||||||
let log_level = config.log_level.as_deref().unwrap_or("info");
|
let log_level = config.log_level.as_deref().unwrap_or("info");
|
||||||
init_logging(log_level);
|
init_logging(log_level);
|
||||||
|
|
||||||
// Merge config with args (args take precedence)
|
// Load configuration from file only
|
||||||
let root = args.root.or(config.root).expect("root is required");
|
let root = config.root.expect("root is required");
|
||||||
let cert_path = args.cert.or(config.cert).expect("cert is required");
|
let cert_path = config.cert.expect("cert is required");
|
||||||
let key_path = args.key.or(config.key).expect("key is required");
|
let key_path = config.key.expect("key is required");
|
||||||
let host = args.host.or(config.host).unwrap_or_else(|| "0.0.0.0".to_string());
|
let host = config.host.unwrap_or_else(|| "0.0.0.0".to_string());
|
||||||
let port = args.port.or(config.port).unwrap_or(1965);
|
let port = config.port.unwrap_or(1965);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
@ -139,7 +119,7 @@ async fn main() {
|
||||||
let test_delay = test_processing_delay;
|
let test_delay = test_processing_delay;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Ok(stream) = acceptor.accept(stream).await {
|
if let Ok(stream) = acceptor.accept(stream).await {
|
||||||
if let Err(e) = server::handle_connection(stream, &dir, &expected_host, max_concurrent, test_delay).await {
|
if let Err(e) = server::handle_connection(stream, &dir, &expected_host, port, max_concurrent, test_delay).await {
|
||||||
tracing::error!("Error handling connection: {}", e);
|
tracing::error!("Error handling connection: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,34 @@ pub enum PathResolutionError {
|
||||||
NotFound,
|
NotFound,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_gemini_url(request: &str, expected_host: &str) -> Result<String, ()> {
|
pub fn parse_gemini_url(request: &str, expected_host: &str, expected_port: u16) -> Result<String, ()> {
|
||||||
if let Some(url) = request.strip_prefix("gemini://") {
|
if let Some(url) = request.strip_prefix("gemini://") {
|
||||||
let host_end = url.find('/').unwrap_or(url.len());
|
let host_port_end = url.find('/').unwrap_or(url.len());
|
||||||
let host = &url[..host_end];
|
let host_port = &url[..host_port_end];
|
||||||
|
|
||||||
|
// Parse host and port
|
||||||
|
let (host, port_str) = if let Some(colon_pos) = host_port.find(':') {
|
||||||
|
let host = &host_port[..colon_pos];
|
||||||
|
let port_str = &host_port[colon_pos + 1..];
|
||||||
|
(host, Some(port_str))
|
||||||
|
} else {
|
||||||
|
(host_port, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate host
|
||||||
if host != expected_host {
|
if host != expected_host {
|
||||||
return Err(()); // Hostname mismatch
|
return Err(()); // Hostname mismatch
|
||||||
}
|
}
|
||||||
let path = if host_end < url.len() { &url[host_end..] } else { "/" };
|
|
||||||
|
// Validate port
|
||||||
|
let port = port_str
|
||||||
|
.and_then(|p| p.parse::<u16>().ok())
|
||||||
|
.unwrap_or(1965);
|
||||||
|
if port != expected_port {
|
||||||
|
return Err(()); // Port mismatch
|
||||||
|
}
|
||||||
|
|
||||||
|
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(())
|
||||||
|
|
@ -69,18 +89,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"), Ok("/".to_string()));
|
assert_eq!(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"), Ok("/posts/test".to_string()));
|
assert_eq!(parse_gemini_url("gemini://gemini.jeena.net/posts/test", "gemini.jeena.net", 1965), Ok("/posts/test".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_gemini_url_invalid_host() {
|
fn test_parse_gemini_url_invalid_host() {
|
||||||
assert!(parse_gemini_url("gemini://foo.com/", "gemini.jeena.net").is_err());
|
assert!(parse_gemini_url("gemini://foo.com/", "gemini.jeena.net", 1965).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_gemini_url_no_prefix() {
|
fn test_parse_gemini_url_no_prefix() {
|
||||||
assert!(parse_gemini_url("http://gemini.jeena.net/", "gemini.jeena.net").is_err());
|
assert!(parse_gemini_url("http://gemini.jeena.net/", "gemini.jeena.net", 1965).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -41,10 +41,11 @@ pub async fn handle_connection(
|
||||||
mut stream: TlsStream<TcpStream>,
|
mut stream: TlsStream<TcpStream>,
|
||||||
dir: &str,
|
dir: &str,
|
||||||
expected_host: &str,
|
expected_host: &str,
|
||||||
|
expected_port: u16,
|
||||||
max_concurrent_requests: usize,
|
max_concurrent_requests: usize,
|
||||||
test_processing_delay: u64,
|
test_processing_delay: u64,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
const MAX_REQUEST_SIZE: usize = 4096;
|
const MAX_REQUEST_SIZE: usize = 1026;
|
||||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|
||||||
let mut request_buf = Vec::new();
|
let mut request_buf = Vec::new();
|
||||||
|
|
@ -96,7 +97,7 @@ pub async fn handle_connection(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse Gemini URL
|
// Parse Gemini URL
|
||||||
let path = match parse_gemini_url(&request, expected_host) {
|
let path = match parse_gemini_url(&request, expected_host, expected_port) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
logger.log_error(59, "Invalid URL format");
|
logger.log_error(59, "Invalid URL format");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue