Skip to main content

rust_serv/proxy/
config.rs

1//! Proxy configuration
2
3use std::time::Duration;
4
5/// Proxy configuration
6#[derive(Debug, Clone)]
7pub struct ProxyConfig {
8    /// Path pattern to match (e.g., "/api")
9    pub path: String,
10    /// Target URL (e.g., "http://localhost:3000")
11    pub target: String,
12    /// Strip the matched path prefix before forwarding
13    pub strip_prefix: bool,
14    /// Connection timeout
15    pub timeout: Duration,
16    /// Preserve Host header
17    pub preserve_host: bool,
18    /// Follow redirects
19    pub follow_redirects: bool,
20}
21
22impl ProxyConfig {
23    /// Create a new proxy config
24    pub fn new(path: impl Into<String>, target: impl Into<String>) -> Self {
25        Self {
26            path: path.into(),
27            target: target.into(),
28            strip_prefix: true,
29            timeout: Duration::from_secs(30),
30            preserve_host: false,
31            follow_redirects: false,
32        }
33    }
34
35    /// Set strip prefix
36    pub fn with_strip_prefix(mut self, strip: bool) -> Self {
37        self.strip_prefix = strip;
38        self
39    }
40
41    /// Set timeout
42    pub fn with_timeout(mut self, timeout: Duration) -> Self {
43        self.timeout = timeout;
44        self
45    }
46
47    /// Set preserve host
48    pub fn with_preserve_host(mut self, preserve: bool) -> Self {
49        self.preserve_host = preserve;
50        self
51    }
52
53    /// Set follow redirects
54    pub fn with_follow_redirects(mut self, follow: bool) -> Self {
55        self.follow_redirects = follow;
56        self
57    }
58
59    /// Check if a path matches this proxy rule
60    pub fn matches(&self, path: &str) -> bool {
61        path.starts_with(&self.path)
62    }
63
64    /// Build target URL for a request path
65    pub fn build_target_url(&self, request_path: &str) -> String {
66        let target_path = if self.strip_prefix && request_path.starts_with(&self.path) {
67            &request_path[self.path.len()..]
68        } else {
69            request_path
70        };
71
72        // Ensure target_path starts with /
73        let target_path = if target_path.starts_with('/') {
74            target_path.to_string()
75        } else if target_path.is_empty() {
76            "/".to_string()
77        } else {
78            format!("/{}", target_path)
79        };
80
81        // Combine target and path
82        let target = self.target.trim_end_matches('/');
83        format!("{}{}", target, target_path)
84    }
85
86    /// Parse target into scheme, host, port
87    pub fn parse_target(&self) -> Result<(String, String, Option<u16>), String> {
88        let target = &self.target;
89        
90        // Extract scheme
91        let scheme_end = target.find("://").ok_or("Invalid target URL: missing scheme")?;
92        let scheme = target[..scheme_end].to_lowercase();
93        
94        let rest = &target[scheme_end + 3..];
95        
96        // Extract host and port
97        let (host, port) = if let Some(port_start) = rest.find(':') {
98            let host = rest[..port_start].to_string();
99            let port_str = &rest[port_start + 1..];
100            let port = port_str.parse::<u16>().map_err(|_| "Invalid port")?;
101            (host, Some(port))
102        } else {
103            (rest.to_string(), None)
104        };
105        
106        Ok((scheme, host, port))
107    }
108}
109
110impl Default for ProxyConfig {
111    fn default() -> Self {
112        Self::new("/api", "http://localhost:3000")
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_proxy_config_creation() {
122        let config = ProxyConfig::new("/api", "http://localhost:3000");
123        assert_eq!(config.path, "/api");
124        assert_eq!(config.target, "http://localhost:3000");
125        assert!(config.strip_prefix);
126    }
127
128    #[test]
129    fn test_proxy_config_with_strip_prefix() {
130        let config = ProxyConfig::new("/api", "http://localhost:3000")
131            .with_strip_prefix(false);
132        assert!(!config.strip_prefix);
133    }
134
135    #[test]
136    fn test_proxy_config_with_timeout() {
137        let config = ProxyConfig::new("/api", "http://localhost:3000")
138            .with_timeout(Duration::from_secs(60));
139        assert_eq!(config.timeout, Duration::from_secs(60));
140    }
141
142    #[test]
143    fn test_proxy_config_with_preserve_host() {
144        let config = ProxyConfig::new("/api", "http://localhost:3000")
145            .with_preserve_host(true);
146        assert!(config.preserve_host);
147    }
148
149    #[test]
150    fn test_proxy_config_with_follow_redirects() {
151        let config = ProxyConfig::new("/api", "http://localhost:3000")
152            .with_follow_redirects(true);
153        assert!(config.follow_redirects);
154    }
155
156    #[test]
157    fn test_matches_exact() {
158        let config = ProxyConfig::new("/api", "http://localhost:3000");
159        assert!(config.matches("/api"));
160        assert!(config.matches("/api/users"));
161        assert!(config.matches("/api/v1/data"));
162    }
163
164    #[test]
165    fn test_matches_no_match() {
166        let config = ProxyConfig::new("/api", "http://localhost:3000");
167        assert!(!config.matches("/"));
168        assert!(!config.matches("/other"));
169        // Note: /apiv1 DOES match /api because it starts with /api
170        // This is expected behavior for prefix matching
171    }
172
173    #[test]
174    fn test_build_target_url_strip_prefix() {
175        let config = ProxyConfig::new("/api", "http://localhost:3000");
176        
177        assert_eq!(config.build_target_url("/api"), "http://localhost:3000/");
178        assert_eq!(config.build_target_url("/api/users"), "http://localhost:3000/users");
179        assert_eq!(config.build_target_url("/api/v1/data"), "http://localhost:3000/v1/data");
180    }
181
182    #[test]
183    fn test_build_target_url_no_strip() {
184        let config = ProxyConfig::new("/api", "http://localhost:3000")
185            .with_strip_prefix(false);
186        
187        assert_eq!(config.build_target_url("/api"), "http://localhost:3000/api");
188        assert_eq!(config.build_target_url("/api/users"), "http://localhost:3000/api/users");
189    }
190
191    #[test]
192    fn test_build_target_url_trailing_slash() {
193        let config = ProxyConfig::new("/api/", "http://localhost:3000/");
194        
195        let url = config.build_target_url("/api/users");
196        assert!(url.starts_with("http://localhost:3000"));
197        assert!(url.ends_with("/users"));
198    }
199
200    #[test]
201    fn test_parse_target_http() {
202        let config = ProxyConfig::new("/api", "http://localhost:3000");
203        let (scheme, host, port) = config.parse_target().unwrap();
204        
205        assert_eq!(scheme, "http");
206        assert_eq!(host, "localhost");
207        assert_eq!(port, Some(3000));
208    }
209
210    #[test]
211    fn test_parse_target_https() {
212        let config = ProxyConfig::new("/api", "https://example.com");
213        let (scheme, host, port) = config.parse_target().unwrap();
214        
215        assert_eq!(scheme, "https");
216        assert_eq!(host, "example.com");
217        assert_eq!(port, None);
218    }
219
220    #[test]
221    fn test_parse_target_with_port() {
222        let config = ProxyConfig::new("/api", "http://backend:8080");
223        let (scheme, host, port) = config.parse_target().unwrap();
224        
225        assert_eq!(scheme, "http");
226        assert_eq!(host, "backend");
227        assert_eq!(port, Some(8080));
228    }
229
230    #[test]
231    fn test_parse_target_invalid() {
232        let config = ProxyConfig::new("/api", "not-a-url");
233        assert!(config.parse_target().is_err());
234    }
235
236    #[test]
237    fn test_parse_target_invalid_port() {
238        let config = ProxyConfig::new("/api", "http://localhost:abc");
239        assert!(config.parse_target().is_err());
240    }
241
242    #[test]
243    fn test_default() {
244        let config = ProxyConfig::default();
245        assert_eq!(config.path, "/api");
246        assert_eq!(config.target, "http://localhost:3000");
247    }
248
249    #[test]
250    fn test_matches_empty_path() {
251        let config = ProxyConfig::new("/", "http://localhost:3000");
252        assert!(config.matches("/"));
253        assert!(config.matches("/anything"));
254    }
255
256    #[test]
257    fn test_build_target_url_root_path() {
258        let config = ProxyConfig::new("/", "http://localhost:3000");
259        
260        assert_eq!(config.build_target_url("/"), "http://localhost:3000/");
261        assert_eq!(config.build_target_url("/users"), "http://localhost:3000/users");
262    }
263}