use crate::route_types::*; use crate::tls_types::*; /// Create a simple HTTP forwarding route. /// Equivalent to SmartProxy's `createHttpRoute()`. pub fn create_http_route( domains: impl Into, target_host: impl Into, target_port: u16, ) -> RouteConfig { RouteConfig { id: None, route_match: RouteMatch { ports: PortRange::Single(80), domains: Some(domains.into()), path: None, client_ip: None, tls_version: None, headers: None, }, action: RouteAction { action_type: RouteActionType::Forward, targets: Some(vec![RouteTarget { target_match: None, host: HostSpec::Single(target_host.into()), port: PortSpec::Fixed(target_port), tls: None, websocket: None, load_balancing: None, send_proxy_protocol: None, headers: None, advanced: None, priority: None, }]), tls: None, websocket: None, load_balancing: None, advanced: None, options: None, forwarding_engine: None, nftables: None, send_proxy_protocol: None, }, headers: None, security: None, name: None, description: None, priority: None, tags: None, enabled: None, } } /// Create an HTTPS termination route. /// Equivalent to SmartProxy's `createHttpsTerminateRoute()`. pub fn create_https_terminate_route( domains: impl Into, target_host: impl Into, target_port: u16, ) -> RouteConfig { let mut route = create_http_route(domains, target_host, target_port); route.route_match.ports = PortRange::Single(443); route.action.tls = Some(RouteTls { mode: TlsMode::Terminate, certificate: Some(CertificateSpec::Auto("auto".to_string())), acme: None, versions: None, ciphers: None, honor_cipher_order: None, session_timeout: None, }); route } /// Create a TLS passthrough route. /// Equivalent to SmartProxy's `createHttpsPassthroughRoute()`. pub fn create_https_passthrough_route( domains: impl Into, target_host: impl Into, target_port: u16, ) -> RouteConfig { let mut route = create_http_route(domains, target_host, target_port); route.route_match.ports = PortRange::Single(443); route.action.tls = Some(RouteTls { mode: TlsMode::Passthrough, certificate: None, acme: None, versions: None, ciphers: None, honor_cipher_order: None, session_timeout: None, }); route } /// Create an HTTP-to-HTTPS redirect route. /// Equivalent to SmartProxy's `createHttpToHttpsRedirect()`. pub fn create_http_to_https_redirect( domains: impl Into, ) -> RouteConfig { let domains = domains.into(); RouteConfig { id: None, route_match: RouteMatch { ports: PortRange::Single(80), domains: Some(domains), path: None, client_ip: None, tls_version: None, headers: None, }, action: RouteAction { action_type: RouteActionType::Forward, targets: None, tls: None, websocket: None, load_balancing: None, advanced: Some(RouteAdvanced { timeout: None, headers: None, keep_alive: None, static_files: None, test_response: Some(RouteTestResponse { status: 301, headers: { let mut h = std::collections::HashMap::new(); h.insert("Location".to_string(), "https://{domain}{path}".to_string()); h }, body: String::new(), }), url_rewrite: None, }), options: None, forwarding_engine: None, nftables: None, send_proxy_protocol: None, }, headers: None, security: None, name: Some("HTTP to HTTPS Redirect".to_string()), description: None, priority: None, tags: None, enabled: None, } } /// Create a complete HTTPS server with HTTP redirect. /// Equivalent to SmartProxy's `createCompleteHttpsServer()`. pub fn create_complete_https_server( domain: impl Into, target_host: impl Into, target_port: u16, ) -> Vec { let domain = domain.into(); let target_host = target_host.into(); vec![ create_http_to_https_redirect(DomainSpec::Single(domain.clone())), create_https_terminate_route( DomainSpec::Single(domain), target_host, target_port, ), ] } /// Create a load balancer route. /// Equivalent to SmartProxy's `createLoadBalancerRoute()`. pub fn create_load_balancer_route( domains: impl Into, targets: Vec<(String, u16)>, tls: Option, ) -> RouteConfig { let route_targets: Vec = targets .into_iter() .map(|(host, port)| RouteTarget { target_match: None, host: HostSpec::Single(host), port: PortSpec::Fixed(port), tls: None, websocket: None, load_balancing: None, send_proxy_protocol: None, headers: None, advanced: None, priority: None, }) .collect(); let port = if tls.is_some() { 443 } else { 80 }; RouteConfig { id: None, route_match: RouteMatch { ports: PortRange::Single(port), domains: Some(domains.into()), path: None, client_ip: None, tls_version: None, headers: None, }, action: RouteAction { action_type: RouteActionType::Forward, targets: Some(route_targets), tls, websocket: None, load_balancing: Some(RouteLoadBalancing { algorithm: LoadBalancingAlgorithm::RoundRobin, health_check: None, }), advanced: None, options: None, forwarding_engine: None, nftables: None, send_proxy_protocol: None, }, headers: None, security: None, name: Some("Load Balancer".to_string()), description: None, priority: None, tags: None, enabled: None, } } // Convenience conversions for DomainSpec impl From<&str> for DomainSpec { fn from(s: &str) -> Self { DomainSpec::Single(s.to_string()) } } impl From for DomainSpec { fn from(s: String) -> Self { DomainSpec::Single(s) } } impl From> for DomainSpec { fn from(v: Vec) -> Self { DomainSpec::List(v) } } impl From> for DomainSpec { fn from(v: Vec<&str>) -> Self { DomainSpec::List(v.into_iter().map(|s| s.to_string()).collect()) } } #[cfg(test)] mod tests { use super::*; use crate::tls_types::TlsMode; #[test] fn test_create_http_route() { let route = create_http_route("example.com", "localhost", 8080); assert_eq!(route.route_match.ports.to_ports(), vec![80]); let domains = route.route_match.domains.as_ref().unwrap().to_vec(); assert_eq!(domains, vec!["example.com"]); let target = &route.action.targets.as_ref().unwrap()[0]; assert_eq!(target.host.first(), "localhost"); assert_eq!(target.port.resolve(80), 8080); assert!(route.action.tls.is_none()); } #[test] fn test_create_https_terminate_route() { let route = create_https_terminate_route("api.example.com", "backend", 3000); assert_eq!(route.route_match.ports.to_ports(), vec![443]); let tls = route.action.tls.as_ref().unwrap(); assert_eq!(tls.mode, TlsMode::Terminate); assert!(tls.certificate.as_ref().unwrap().is_auto()); } #[test] fn test_create_https_passthrough_route() { let route = create_https_passthrough_route("secure.example.com", "backend", 443); assert_eq!(route.route_match.ports.to_ports(), vec![443]); let tls = route.action.tls.as_ref().unwrap(); assert_eq!(tls.mode, TlsMode::Passthrough); assert!(tls.certificate.is_none()); } #[test] fn test_create_http_to_https_redirect() { let route = create_http_to_https_redirect("example.com"); assert_eq!(route.route_match.ports.to_ports(), vec![80]); assert!(route.action.targets.is_none()); let test_response = route.action.advanced.as_ref().unwrap().test_response.as_ref().unwrap(); assert_eq!(test_response.status, 301); assert!(test_response.headers.contains_key("Location")); } #[test] fn test_create_complete_https_server() { let routes = create_complete_https_server("example.com", "backend", 8080); assert_eq!(routes.len(), 2); // First route is HTTP redirect assert_eq!(routes[0].route_match.ports.to_ports(), vec![80]); // Second route is HTTPS terminate assert_eq!(routes[1].route_match.ports.to_ports(), vec![443]); } #[test] fn test_create_load_balancer_route() { let targets = vec![ ("backend1".to_string(), 8080), ("backend2".to_string(), 8080), ("backend3".to_string(), 8080), ]; let route = create_load_balancer_route("*.example.com", targets, None); assert_eq!(route.route_match.ports.to_ports(), vec![80]); assert_eq!(route.action.targets.as_ref().unwrap().len(), 3); let lb = route.action.load_balancing.as_ref().unwrap(); assert_eq!(lb.algorithm, LoadBalancingAlgorithm::RoundRobin); } #[test] fn test_domain_spec_from_str() { let spec: DomainSpec = "example.com".into(); assert_eq!(spec.to_vec(), vec!["example.com"]); } #[test] fn test_domain_spec_from_vec() { let spec: DomainSpec = vec!["a.com", "b.com"].into(); assert_eq!(spec.to_vec(), vec!["a.com", "b.com"]); } }