use anyhow::Result; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::PathBuf; /// Complete Caddy configuration structure for 100% compatibility #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CaddyConfig { /// Global admin settings pub admin: Option, /// App configurations pub apps: Apps, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AdminConfig { /// Admin endpoint listen address pub listen: Option, /// API origins pub origins: Option>, /// Remote admin endpoint pub remote: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RemoteAdmin { pub endpoint: String, pub access_id: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Apps { /// HTTP app configuration pub http: HttpApp, /// TLS app configuration pub tls: Option, /// PKI app configuration pub pki: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HttpApp { /// HTTP servers configuration pub servers: HashMap, /// Grace period for graceful shutdown pub grace_period: Option, /// Shutdown delay pub shutdown_delay: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HttpServer { /// Listen addresses pub listen: Vec, /// Routes configuration pub routes: Vec, /// Error handling pub errors: Option, /// TLS connection policies pub tls_connection_policies: Option>, /// Automatic HTTPS pub automatic_https: Option, /// Protocol configuration pub protocols: Option>, /// Strict SNI host matching pub strict_sni_host: Option, /// Request timeout pub request_timeout: Option, /// Read timeout pub read_timeout: Option, /// Read header timeout pub read_header_timeout: Option, /// Write timeout pub write_timeout: Option, /// Idle timeout pub idle_timeout: Option, /// Max header bytes pub max_header_bytes: Option, /// Enable H2C pub allow_h2c: Option, /// Experimental HTTP/3 pub experimental_http3: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Route { /// Route matchers #[serde(rename = "match")] pub match_rules: Option>, /// Handler chain pub handle: Vec, /// Terminal route (default: true) pub terminal: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "handler")] pub enum Handler { /// Authentication handler #[serde(rename = "authentication")] Authentication { providers: HashMap, }, /// Basic auth handler #[serde(rename = "http_basic_auth")] BasicAuth { accounts: Vec, realm: Option, hash: Option, }, /// Static file server #[serde(rename = "file_server")] FileServer { root: Option, hide: Option>, index_names: Option>, browse: Option, precompressed: Option, status_code: Option, canonical_uris: Option, pass_thru: Option, }, /// Reverse proxy #[serde(rename = "reverse_proxy")] ReverseProxy { upstreams: Vec, load_balancing: Option, health_checks: Option, circuit_breaker: Option, headers: Option, transport: Option, handle_response: Option>, trusted_proxies: Option>, replace_status: Option>, buffer_requests: Option, buffer_responses: Option, max_buffer_size: Option, stream_timeout: Option, stream_close_delay: Option, flush_interval: Option, }, /// Static response #[serde(rename = "static_response")] StaticResponse { status_code: Option, headers: Option>>, body: Option, close: Option, }, /// Redirect handler #[serde(rename = "redirect")] Redirect { to: Option, status_code: Option, }, /// Rewrite handler #[serde(rename = "rewrite")] Rewrite { uri: Option, strip_path_prefix: Option, strip_path_suffix: Option, uri_substring: Option>, method: Option, }, /// Headers handler #[serde(rename = "headers")] Headers { request: Option, response: Option, }, /// Copy response headers handler #[serde(rename = "copy_response_headers")] CopyResponseHeaders { include: Option>, exclude: Option>, }, /// Request body handler #[serde(rename = "request_body")] RequestBody { max_size: Option, }, /// Response compression #[serde(rename = "encode")] Encode { encodings: Option>, prefer: Option>, minimum_length: Option, }, /// Template handler #[serde(rename = "templates")] Templates { file_root: Option, mime_types: Option>, delimiters: Option>, }, /// Subroute handler #[serde(rename = "subroute")] Subroute { routes: Vec, errors: Option, }, /// Error handler #[serde(rename = "error")] Error { error: Option, status_code: Option, }, /// Map handler #[serde(rename = "map")] Map { source: String, destinations: HashMap, default: Option, }, /// Rate limit handler #[serde(rename = "rate_limit")] RateLimit { key: Option, rate: Option, burst: Option, window: Option, }, /// IP whitelist handler #[serde(rename = "ip_whitelist")] IpWhitelist { source: Option, rules: Vec, }, /// Request ID handler #[serde(rename = "request_id")] RequestId { header_name: Option, size: Option, }, /// Metrics handler #[serde(rename = "metrics")] Metrics { path: Option, }, /// Health check handler #[serde(rename = "health")] Health { path: Option, }, /// Vars handler #[serde(rename = "vars")] Vars { #[serde(flatten)] variables: HashMap, }, /// Custom handler #[serde(rename = "custom")] Custom { module: String, #[serde(flatten)] config: HashMap, }, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Matcher { /// Host matcher Host(Vec), /// Path matcher Path(Vec), /// Path regexp matcher PathRegexp(Vec), /// Method matcher Method(Vec), /// Query matcher Query(HashMap>), /// Header matcher Header(HashMap>), /// Header regexp matcher HeaderRegexp(HashMap>), /// Remote IP matcher RemoteIp { ranges: Vec, forwarded: Option, }, /// Protocol matcher Protocol(String), /// File matcher File { root: Option, files: Vec, try_files: Option>, try_policy: Option, split_path: Option>, }, /// Expression matcher Expression { expr: String, }, /// Vars matcher Vars(HashMap), /// Not matcher Not { #[serde(rename = "match")] matcher: Box, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthProvider { #[serde(flatten)] pub config: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BasicAuthAccount { pub username: String, pub password: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BasicAuthHash { pub algorithm: Option, pub cost: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BrowseConfig { pub template: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PrecompressedConfig { pub encodings: Option>, pub min_length: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Upstream { pub dial: String, pub max_requests: Option, pub max_requests_per_host: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LoadBalancing { pub selection_policy: Option, pub try_duration: Option, pub try_interval: Option, pub unhealthy_request_count: Option, pub unhealthy_status: Option>, pub unhealthy_latency: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SelectionPolicy { RoundRobin, LeastConn, Random, First, IpHash, UriHash, Header, Cookie, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HealthChecks { pub active: Option, pub passive: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ActiveHealthCheck { pub uri: String, pub port: Option, pub headers: Option>>, pub interval: Option, pub timeout: Option, pub max_size: Option, pub expect_status: Option, pub expect_body: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PassiveHealthCheck { pub unhealthy_status: Option>, pub unhealthy_latency: Option, pub unhealthy_request_count: Option, pub healthy_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CircuitBreaker { pub trip_duration: Option, pub recovery_duration: Option, pub failure_threshold: Option, pub success_threshold: Option, pub latency_threshold: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HeaderOperations { pub add: Option>>, pub set: Option>>, pub delete: Option>, pub replace: Option>>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HeaderReplacement { pub search: String, pub replace: String, pub search_regexp: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Transport { pub protocol: Option, pub tls: Option, pub keep_alive: Option, pub compression: Option, pub max_conns_per_host: Option, pub dial_timeout: Option, pub dial_fallback_delay: Option, pub response_header_timeout: Option, pub expect_continue_timeout: Option, pub max_response_header_size: Option, pub write_buffer_size: Option, pub read_buffer_size: Option, pub versions: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TransportTls { pub client_certificate_file: Option, pub client_certificate_key_file: Option, pub client_certificate_automate: Option, pub root_ca_pool: Option>, pub root_ca_pem_files: Option>, pub server_name: Option, pub insecure_skip_verify: Option, pub handshake_timeout: Option, pub versions: Option>, pub cipher_suites: Option>, pub curves: Option>, pub alpn: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KeepAlive { pub enabled: Option, pub probe_interval: Option, pub max_idle_conns: Option, pub max_idle_conns_per_host: Option, pub idle_conn_timeout: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseHandler { #[serde(rename = "match")] pub match_rules: Option, pub routes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResponseMatcher { pub status_code: Option>, pub headers: Option>>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StatusReplacement { pub status_code: i32, pub with: i32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UriSubstring { pub find: String, pub replace: String, pub limit: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EncodingConfig { #[serde(flatten)] pub config: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct IpRule { pub action: String, pub rule: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ErrorHandling { pub routes: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlsConnectionPolicy { #[serde(rename = "match")] pub match_rules: Option, pub certificate_selection: Option, pub cipher_suites: Option>, pub curves: Option>, pub alpn: Option>, pub protocols: Option, pub client_authentication: Option, pub insecure_secrets_log: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlsConnectionMatcher { pub sni: Option>, pub remote_ip: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RemoteIpMatcher { pub ranges: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificateSelection { pub any_tag: Option>, pub all_tags: Option>, pub public_key_algorithm: Option, pub serial_number: Option, pub subject_organization: Option, pub subject: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProtocolRange { pub min: Option, pub max: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClientAuthentication { pub mode: Option, pub trusted_ca_certs: Option>, pub trusted_ca_certs_pem_files: Option>, pub trusted_leaf_certs: Option>, pub trusted_leaf_certs_pem_files: Option>, pub verify_client_certificate: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutomaticHttpsConfig { pub disable: Option, pub disable_redirects: Option, pub disable_certs: Option, pub ignore_loaded_certs: Option, pub skip: Option>, pub skip_certificates: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlsApp { pub automation: Option, pub session_tickets: Option, pub certificates: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutomationConfig { pub policies: Vec, pub on_demand: Option, pub ocsp_interval: Option, pub renew_ahead: Option, pub storage: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AutomationPolicy { pub subjects: Option>, pub issuer: Option, pub must_staple: Option, pub key_type: Option, pub storage: Option, pub on_demand: Option, pub disable: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "module")] pub enum IssuerConfig { /// ACME issuer #[serde(rename = "acme")] Acme { ca: Option, test_ca: Option, email: Option, account_key_pem: Option, external_account: Option, challenges: Option, preferred_chains: Option, must_staple: Option, trusted_roots_pem_files: Option>, }, /// Internal issuer #[serde(rename = "internal")] Internal { ca: Option, lifetime: Option, sign_with_root: Option, }, /// External issuer #[serde(rename = "external")] External { command: Vec, timeout: Option, env: Option>, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ExternalAccount { pub key_id: String, pub mac_key: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChallengeConfig { pub http: Option, pub dns: Option, pub tls_alpn: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HttpChallengeConfig { pub disabled: Option, pub alternate_port: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DnsChallengeConfig { pub provider: String, pub disabled: Option, pub propagation_delay: Option, pub propagation_timeout: Option, pub resolvers: Option>, pub ttl: Option, #[serde(flatten)] pub config: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TlsAlpnChallengeConfig { pub disabled: Option, pub alternate_port: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PreferredChains { pub smallest: Option, pub root_common_name: Option>, pub any_common_name: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OnDemandConfig { pub rate_limit: Option, pub ask: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RateLimitConfig { pub interval: String, pub burst: i32, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StorageConfig { pub module: String, #[serde(flatten)] pub config: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SessionTicketsConfig { pub key_source: Option, pub rotation_interval: Option, pub max_keys: Option, pub disabled: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KeySourceConfig { pub module: String, #[serde(flatten)] pub config: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificatesConfig { pub load_files: Option>, pub load_folders: Option>, pub load_pem: Option>, pub automate: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificateFile { pub certificate: String, pub key: String, pub format: Option, pub tags: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PemCertificate { pub certificate: String, pub key: String, pub tags: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PkiApp { pub certificate_authorities: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificateAuthority { pub name: Option, pub root_common_name: Option, pub intermediate_common_name: Option, pub intermediate_lifetime: Option, pub root: Option, pub intermediate: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CertificateConfig { pub format: Option, pub common_name: Option, pub country: Option>, pub organization: Option>, pub organizational_unit: Option>, pub locality: Option>, pub province: Option>, pub street_address: Option>, pub postal_code: Option>, } /// Converter from Caddy config to Quantum config pub struct CaddyConverter; impl CaddyConverter { /// Convert Caddy configuration to Quantum configuration pub fn convert(caddy_config: &CaddyConfig) -> Result { let mut servers = HashMap::new(); // Convert each HTTP server for (server_name, http_server) in &caddy_config.apps.http.servers { let quantum_server = crate::config::Server { listen: http_server.listen.clone(), routes: Self::convert_routes(&http_server.routes)?, automatic_https: crate::config::AutomaticHttps::default(), tls: Self::convert_tls_config(http_server)?, }; servers.insert(server_name.clone(), quantum_server); } Ok(crate::config::Config { admin: crate::config::AdminConfig { listen: caddy_config.admin.as_ref().and_then(|a| a.listen.clone()), }, apps: crate::config::Apps { http: crate::config::HttpApp { servers }, }, }) } fn convert_routes(caddy_routes: &[Route]) -> Result> { caddy_routes .iter() .map(|route| { Ok(crate::config::Route { handle: Self::convert_handlers(&route.handle)?, match_rules: route.match_rules.as_ref().map(|m| Self::convert_matchers(m)), }) }) .collect() } fn convert_handlers(caddy_handlers: &[Handler]) -> Result> { caddy_handlers .iter() .map(|handler| match handler { Handler::BasicAuth { accounts, realm, .. } => { let mut quantum_accounts = HashMap::new(); for account in accounts { quantum_accounts.insert(account.username.clone(), account.password.clone()); } Ok(crate::config::Handler::BasicAuth { accounts: quantum_accounts, realm: realm.clone(), }) } Handler::FileServer { root, index_names, .. } => { Ok(crate::config::Handler::FileServer { root: root.clone().unwrap_or_else(|| ".".to_string()), try_files: None, index: index_names.clone(), browse: None, }) } Handler::ReverseProxy { upstreams, load_balancing, .. } => { let quantum_upstreams: Vec = upstreams .iter() .map(|up| crate::config::Upstream { dial: up.dial.clone(), max_requests: None, unhealthy_request_count: 0, }) .collect(); let quantum_lb = load_balancing.as_ref().map(|lb| { crate::config::LoadBalancing { selection_policy: match lb.selection_policy { Some(SelectionPolicy::RoundRobin) => crate::config::SelectionPolicy::RoundRobin, Some(SelectionPolicy::LeastConn) => crate::config::SelectionPolicy::LeastConn, Some(SelectionPolicy::Random) => crate::config::SelectionPolicy::Random, Some(SelectionPolicy::IpHash) => crate::config::SelectionPolicy::IpHash, _ => crate::config::SelectionPolicy::RoundRobin, }, } }); Ok(crate::config::Handler::ReverseProxy { upstreams: quantum_upstreams, load_balancing: quantum_lb.unwrap_or_default(), health_checks: None, // Could be implemented }) } Handler::StaticResponse { status_code, headers, body, .. } => { let quantum_headers = headers.as_ref().map(|h| { h.iter() .map(|(k, v)| (k.clone(), vec![v.join(", ")])) .collect() }); Ok(crate::config::Handler::StaticResponse { status_code: status_code.map(|s| s as u16), headers: quantum_headers, body: body.clone(), }) } Handler::Redirect { to, status_code } => { Ok(crate::config::Handler::Redirect { to: to.clone().unwrap_or_default(), status_code: status_code.map(|s| s as u16), }) } Handler::Rewrite { uri, .. } => { Ok(crate::config::Handler::Rewrite { uri: uri.clone().unwrap_or_default(), }) } Handler::Headers { request, response } => { // Convert header operations to quantum format Ok(crate::config::Handler::Headers { request: request.clone().map(|_| HashMap::new()), // Simplified response: response.clone().map(|_| HashMap::new()), // Simplified }) } Handler::Error { status_code, .. } => { Ok(crate::config::Handler::Error { status_code: status_code.map(|s| s as u16), message: None, }) } _ => { // For handlers we don't support yet, create a static response Ok(crate::config::Handler::StaticResponse { status_code: Some(501), headers: None, body: Some("Handler not yet implemented".to_string()), }) } }) .collect() } fn convert_matchers(_caddy_matchers: &[Matcher]) -> Vec { // Simplified matcher conversion - could be expanded vec![] } fn convert_tls_config(_http_server: &HttpServer) -> Result> { // Simplified TLS conversion - could be expanded Ok(None) } /// Parse Caddyfile format into CaddyConfig pub fn parse_caddyfile(content: &str) -> Result { // This is a simplified Caddyfile parser // In a real implementation, this would parse the Caddyfile syntax let mut servers = HashMap::new(); // For now, create a basic server servers.insert("default".to_string(), HttpServer { listen: vec![":80".to_string(), ":443".to_string()], routes: vec![Route { match_rules: None, handle: vec![Handler::FileServer { root: Some("/var/www".to_string()), hide: None, index_names: Some(vec!["index.html".to_string()]), browse: None, precompressed: None, status_code: None, canonical_uris: None, pass_thru: None, }], terminal: Some(true), }], errors: None, tls_connection_policies: None, automatic_https: None, protocols: None, strict_sni_host: None, request_timeout: None, read_timeout: None, read_header_timeout: None, write_timeout: None, idle_timeout: None, max_header_bytes: None, allow_h2c: None, experimental_http3: None, }); Ok(CaddyConfig { admin: Some(AdminConfig { listen: Some("localhost:2019".to_string()), origins: None, remote: None, }), apps: Apps { http: HttpApp { servers, grace_period: None, shutdown_delay: None, }, tls: None, pki: None, }, }) } /// Load and convert Caddy configuration from file pub fn load_and_convert(path: &PathBuf) -> Result { let content = std::fs::read_to_string(path)?; let caddy_config = if path.extension().and_then(|s| s.to_str()) == Some("json") { serde_json::from_str::(&content)? } else { // Assume Caddyfile format Self::parse_caddyfile(&content)? }; Self::convert(&caddy_config) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_caddy_config_deserialization() { let config_json = r#"{ "apps": { "http": { "servers": { "example": { "listen": [":80", ":443"], "routes": [{ "handle": [{ "handler": "file_server", "root": "/var/www" }] }] } } } } }"#; let result: Result = serde_json::from_str(config_json); assert!(result.is_ok()); } #[test] fn test_caddy_converter() { let caddy_config = CaddyConfig { admin: None, apps: Apps { http: HttpApp { servers: [( "test".to_string(), HttpServer { listen: vec![":8080".to_string()], routes: vec![Route { match_rules: None, handle: vec![Handler::StaticResponse { status_code: Some(200), headers: None, body: Some("Hello World".to_string()), close: None, }], terminal: Some(true), }], errors: None, tls_connection_policies: None, automatic_https: None, protocols: None, strict_sni_host: None, request_timeout: None, read_timeout: None, read_header_timeout: None, write_timeout: None, idle_timeout: None, max_header_bytes: None, allow_h2c: None, experimental_http3: None, }, )] .into_iter() .collect(), grace_period: None, shutdown_delay: None, }, tls: None, pki: None, }, }; let result = CaddyConverter::convert(&caddy_config); assert!(result.is_ok()); } }