use serde::{Deserialize, Serialize}; use crate::route_types::RouteConfig; /// Global ACME configuration options. /// Matches TypeScript: `IAcmeOptions` #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AcmeOptions { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, /// Required when any route uses certificate: 'auto' #[serde(skip_serializing_if = "Option::is_none")] pub email: Option, #[serde(skip_serializing_if = "Option::is_none")] pub environment: Option, /// Alias for email #[serde(skip_serializing_if = "Option::is_none")] pub account_email: Option, /// Port for HTTP-01 challenges (default: 80) #[serde(skip_serializing_if = "Option::is_none")] pub port: Option, /// Use Let's Encrypt production (default: false) #[serde(skip_serializing_if = "Option::is_none")] pub use_production: Option, /// Days before expiry to renew (default: 30) #[serde(skip_serializing_if = "Option::is_none")] pub renew_threshold_days: Option, /// Enable automatic renewal (default: true) #[serde(skip_serializing_if = "Option::is_none")] pub auto_renew: Option, /// Directory to store certificates (default: './certs') #[serde(skip_serializing_if = "Option::is_none")] pub certificate_store: Option, #[serde(skip_serializing_if = "Option::is_none")] pub skip_configured_certs: Option, /// How often to check for renewals (default: 24) #[serde(skip_serializing_if = "Option::is_none")] pub renew_check_interval_hours: Option, } /// ACME environment. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum AcmeEnvironment { Production, Staging, } /// Default target configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DefaultTarget { pub host: String, pub port: u16, } /// Default security configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DefaultSecurity { #[serde(skip_serializing_if = "Option::is_none")] pub ip_allow_list: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub ip_block_list: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub max_connections: Option, } /// Default configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DefaultConfig { #[serde(skip_serializing_if = "Option::is_none")] pub target: Option, #[serde(skip_serializing_if = "Option::is_none")] pub security: Option, #[serde(skip_serializing_if = "Option::is_none")] pub preserve_source_ip: Option, } /// Keep-alive treatment. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum KeepAliveTreatment { Standard, Extended, Immortal, } /// Metrics configuration. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MetricsConfig { #[serde(skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(skip_serializing_if = "Option::is_none")] pub sample_interval_ms: Option, #[serde(skip_serializing_if = "Option::is_none")] pub retention_seconds: Option, } /// RustProxy configuration options. /// Matches TypeScript: `ISmartProxyOptions` /// /// This is the top-level configuration that can be loaded from a JSON file /// or constructed programmatically. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RustProxyOptions { /// The unified configuration array (required) pub routes: Vec, /// Preserve client IP when forwarding #[serde(skip_serializing_if = "Option::is_none")] pub preserve_source_ip: Option, /// List of trusted proxy IPs that can send PROXY protocol #[serde(skip_serializing_if = "Option::is_none")] pub proxy_ips: Option>, /// Global option to accept PROXY protocol #[serde(skip_serializing_if = "Option::is_none")] pub accept_proxy_protocol: Option, /// Global option to send PROXY protocol to all targets #[serde(skip_serializing_if = "Option::is_none")] pub send_proxy_protocol: Option, /// Global/default settings #[serde(skip_serializing_if = "Option::is_none")] pub defaults: Option, // ─── Timeout Settings ──────────────────────────────────────────── /// Timeout for establishing connection to backend (ms), default: 30000 #[serde(skip_serializing_if = "Option::is_none")] pub connection_timeout: Option, /// Timeout for initial data/SNI (ms), default: 60000 #[serde(skip_serializing_if = "Option::is_none")] pub initial_data_timeout: Option, /// Socket inactivity timeout (ms), default: 3600000 #[serde(skip_serializing_if = "Option::is_none")] pub socket_timeout: Option, /// How often to check for inactive connections (ms), default: 60000 #[serde(skip_serializing_if = "Option::is_none")] pub inactivity_check_interval: Option, /// Default max connection lifetime (ms), default: 86400000 #[serde(skip_serializing_if = "Option::is_none")] pub max_connection_lifetime: Option, /// Inactivity timeout (ms), default: 14400000 #[serde(skip_serializing_if = "Option::is_none")] pub inactivity_timeout: Option, /// Maximum time to wait for connections to close during shutdown (ms) #[serde(skip_serializing_if = "Option::is_none")] pub graceful_shutdown_timeout: Option, // ─── Socket Optimization ───────────────────────────────────────── /// Disable Nagle's algorithm (default: true) #[serde(skip_serializing_if = "Option::is_none")] pub no_delay: Option, /// Enable TCP keepalive (default: true) #[serde(skip_serializing_if = "Option::is_none")] pub keep_alive: Option, /// Initial delay before sending keepalive probes (ms) #[serde(skip_serializing_if = "Option::is_none")] pub keep_alive_initial_delay: Option, /// Maximum bytes to buffer during connection setup #[serde(skip_serializing_if = "Option::is_none")] pub max_pending_data_size: Option, // ─── Enhanced Features ─────────────────────────────────────────── /// Disable inactivity checking entirely #[serde(skip_serializing_if = "Option::is_none")] pub disable_inactivity_check: Option, /// Enable TCP keep-alive probes #[serde(skip_serializing_if = "Option::is_none")] pub enable_keep_alive_probes: Option, /// Enable detailed connection logging #[serde(skip_serializing_if = "Option::is_none")] pub enable_detailed_logging: Option, /// Enable TLS handshake debug logging #[serde(skip_serializing_if = "Option::is_none")] pub enable_tls_debug_logging: Option, /// Randomize timeouts to prevent thundering herd #[serde(skip_serializing_if = "Option::is_none")] pub enable_randomized_timeouts: Option, // ─── Rate Limiting ─────────────────────────────────────────────── /// Maximum simultaneous connections from a single IP #[serde(skip_serializing_if = "Option::is_none")] pub max_connections_per_ip: Option, /// Max new connections per minute from a single IP #[serde(skip_serializing_if = "Option::is_none")] pub connection_rate_limit_per_minute: Option, // ─── Keep-Alive Settings ───────────────────────────────────────── /// How to treat keep-alive connections #[serde(skip_serializing_if = "Option::is_none")] pub keep_alive_treatment: Option, /// Multiplier for inactivity timeout for keep-alive connections #[serde(skip_serializing_if = "Option::is_none")] pub keep_alive_inactivity_multiplier: Option, /// Extended lifetime for keep-alive connections (ms) #[serde(skip_serializing_if = "Option::is_none")] pub extended_keep_alive_lifetime: Option, // ─── HttpProxy Integration ─────────────────────────────────────── /// Array of ports to forward to HttpProxy #[serde(skip_serializing_if = "Option::is_none")] pub use_http_proxy: Option>, /// Port where HttpProxy is listening (default: 8443) #[serde(skip_serializing_if = "Option::is_none")] pub http_proxy_port: Option, // ─── Metrics ───────────────────────────────────────────────────── /// Metrics configuration #[serde(skip_serializing_if = "Option::is_none")] pub metrics: Option, // ─── ACME ──────────────────────────────────────────────────────── /// Global ACME configuration #[serde(skip_serializing_if = "Option::is_none")] pub acme: Option, } impl Default for RustProxyOptions { fn default() -> Self { Self { routes: Vec::new(), preserve_source_ip: None, proxy_ips: None, accept_proxy_protocol: None, send_proxy_protocol: None, defaults: None, connection_timeout: None, initial_data_timeout: None, socket_timeout: None, inactivity_check_interval: None, max_connection_lifetime: None, inactivity_timeout: None, graceful_shutdown_timeout: None, no_delay: None, keep_alive: None, keep_alive_initial_delay: None, max_pending_data_size: None, disable_inactivity_check: None, enable_keep_alive_probes: None, enable_detailed_logging: None, enable_tls_debug_logging: None, enable_randomized_timeouts: None, max_connections_per_ip: None, connection_rate_limit_per_minute: None, keep_alive_treatment: None, keep_alive_inactivity_multiplier: None, extended_keep_alive_lifetime: None, use_http_proxy: None, http_proxy_port: None, metrics: None, acme: None, } } } impl RustProxyOptions { /// Load configuration from a JSON file. pub fn from_file(path: &str) -> Result> { let content = std::fs::read_to_string(path)?; let options: Self = serde_json::from_str(&content)?; Ok(options) } /// Get the effective connection timeout in milliseconds. pub fn effective_connection_timeout(&self) -> u64 { self.connection_timeout.unwrap_or(30_000) } /// Get the effective initial data timeout in milliseconds. pub fn effective_initial_data_timeout(&self) -> u64 { self.initial_data_timeout.unwrap_or(60_000) } /// Get the effective socket timeout in milliseconds. pub fn effective_socket_timeout(&self) -> u64 { self.socket_timeout.unwrap_or(3_600_000) } /// Get the effective max connection lifetime in milliseconds. pub fn effective_max_connection_lifetime(&self) -> u64 { self.max_connection_lifetime.unwrap_or(86_400_000) } /// Get all unique ports that routes listen on. pub fn all_listening_ports(&self) -> Vec { let mut ports: Vec = self.routes .iter() .flat_map(|r| r.listening_ports()) .collect(); ports.sort(); ports.dedup(); ports } } #[cfg(test)] mod tests { use super::*; use crate::helpers::*; #[test] fn test_serde_roundtrip_minimal() { let options = RustProxyOptions { routes: vec![create_http_route("example.com", "localhost", 8080)], ..Default::default() }; let json = serde_json::to_string(&options).unwrap(); let parsed: RustProxyOptions = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.routes.len(), 1); } #[test] fn test_serde_roundtrip_full() { let options = RustProxyOptions { routes: vec![ create_http_route("a.com", "backend1", 8080), create_https_passthrough_route("b.com", "backend2", 443), ], connection_timeout: Some(5000), socket_timeout: Some(60000), max_connections_per_ip: Some(100), acme: Some(AcmeOptions { enabled: Some(true), email: Some("admin@example.com".to_string()), environment: Some(AcmeEnvironment::Staging), account_email: None, port: None, use_production: None, renew_threshold_days: None, auto_renew: None, certificate_store: None, skip_configured_certs: None, renew_check_interval_hours: None, }), ..Default::default() }; let json = serde_json::to_string_pretty(&options).unwrap(); let parsed: RustProxyOptions = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.routes.len(), 2); assert_eq!(parsed.connection_timeout, Some(5000)); } #[test] fn test_default_timeouts() { let options = RustProxyOptions::default(); assert_eq!(options.effective_connection_timeout(), 30_000); assert_eq!(options.effective_initial_data_timeout(), 60_000); assert_eq!(options.effective_socket_timeout(), 3_600_000); assert_eq!(options.effective_max_connection_lifetime(), 86_400_000); } #[test] fn test_custom_timeouts() { let options = RustProxyOptions { connection_timeout: Some(5000), initial_data_timeout: Some(10000), socket_timeout: Some(30000), max_connection_lifetime: Some(60000), ..Default::default() }; assert_eq!(options.effective_connection_timeout(), 5000); assert_eq!(options.effective_initial_data_timeout(), 10000); assert_eq!(options.effective_socket_timeout(), 30000); assert_eq!(options.effective_max_connection_lifetime(), 60000); } #[test] fn test_all_listening_ports() { let options = RustProxyOptions { routes: vec![ create_http_route("a.com", "backend", 8080), // port 80 create_https_passthrough_route("b.com", "backend", 443), // port 443 create_http_route("c.com", "backend", 9090), // port 80 (duplicate) ], ..Default::default() }; let ports = options.all_listening_ports(); assert_eq!(ports, vec![80, 443]); } #[test] fn test_camel_case_field_names() { let options = RustProxyOptions { connection_timeout: Some(5000), max_connections_per_ip: Some(100), keep_alive_treatment: Some(KeepAliveTreatment::Extended), ..Default::default() }; let json = serde_json::to_string(&options).unwrap(); assert!(json.contains("connectionTimeout")); assert!(json.contains("maxConnectionsPerIp")); assert!(json.contains("keepAliveTreatment")); } #[test] fn test_deserialize_example_json() { let content = std::fs::read_to_string( concat!(env!("CARGO_MANIFEST_DIR"), "/../../config/example.json") ).unwrap(); let options: RustProxyOptions = serde_json::from_str(&content).unwrap(); assert_eq!(options.routes.len(), 4); let ports = options.all_listening_ports(); assert!(ports.contains(&80)); assert!(ports.contains(&443)); } }