use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct HttpServerConfig {
pub enabled: bool,
pub bind_address: String,
pub port: u16,
pub request_timeout_secs: u64,
pub max_request_size: usize,
pub cors_enabled: bool,
#[serde(default)]
pub cors_allowed_origins: Vec<String>,
pub static_dir: Option<String>,
pub https_enabled: bool,
pub https_cert_file: Option<String>,
pub https_key_file: Option<String>,
}
impl Default for HttpServerConfig {
fn default() -> Self {
Self {
enabled: true,
bind_address: "127.0.0.1".to_string(),
port: 9758,
request_timeout_secs: 60,
max_request_size: 10 * 1024 * 1024, cors_enabled: true,
cors_allowed_origins: Vec::new(),
static_dir: None,
https_enabled: false,
https_cert_file: None,
https_key_file: None,
}
}
}
impl HttpServerConfig {
pub fn validate(&self) -> Result<(), String> {
if self.port == 0 {
return Err("HTTP port cannot be 0".to_string());
}
if self.max_request_size == 0 {
return Err("Max request size must be greater than 0".to_string());
}
for origin in &self.cors_allowed_origins {
Self::validate_origin(origin)?;
}
if self.https_enabled {
if self.https_cert_file.is_none() {
return Err(
"HTTPS certificate file must be specified when HTTPS is enabled".to_string(),
);
}
if self.https_key_file.is_none() {
return Err("HTTPS key file must be specified when HTTPS is enabled".to_string());
}
}
Ok(())
}
pub fn is_https_configured(&self) -> bool {
self.https_enabled && self.https_cert_file.is_some() && self.https_key_file.is_some()
}
pub fn is_strict_cors(&self) -> bool {
self.cors_enabled && !self.cors_allowed_origins.is_empty()
}
fn validate_origin(origin: &str) -> Result<(), String> {
let rest = origin
.strip_prefix("https://")
.or_else(|| origin.strip_prefix("http://"))
.ok_or_else(|| {
format!(
"Invalid CORS origin '{}': scheme must be http or https",
origin
)
})?;
if rest.is_empty() || rest.contains('/') || rest.contains('?') || rest.contains('#') {
return Err(format!(
"Invalid CORS origin '{}': expected http(s)://host[:port] without path",
origin
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_http_server_config_default() {
let config = HttpServerConfig::default();
assert!(config.enabled);
assert_eq!(config.port, 9758);
assert_eq!(config.bind_address, "127.0.0.1");
assert!(config.cors_enabled);
assert!(config.cors_allowed_origins.is_empty());
assert!(!config.https_enabled);
assert!(!config.is_https_configured());
}
#[test]
fn test_http_server_config_validate() {
let config = HttpServerConfig::default();
assert!(config.validate().is_ok());
let invalid_config = HttpServerConfig {
port: 0,
..Default::default()
};
assert!(invalid_config.validate().is_err());
}
#[test]
fn test_http_server_config_https_validation() {
let config = HttpServerConfig {
https_enabled: true,
..Default::default()
};
assert!(config.validate().is_err());
let config = HttpServerConfig {
https_enabled: true,
https_cert_file: Some("cert.pem".to_string()),
https_key_file: Some("key.pem".to_string()),
..Default::default()
};
assert!(config.validate().is_ok());
assert!(config.is_https_configured());
}
#[test]
fn test_cors_origin_validation() {
let mut config = HttpServerConfig::default();
assert!(config.validate().is_ok());
assert!(!config.is_strict_cors());
config.cors_allowed_origins = vec!["https://console.example.com".to_string()];
assert!(config.validate().is_ok());
assert!(config.is_strict_cors());
for bad in [
"console.example.com",
"https://console.example.com/app",
"ftp://console.example.com",
"https://",
] {
config.cors_allowed_origins = vec![bad.to_string()];
assert!(config.validate().is_err(), "origin should fail: {bad}");
}
}
}