rust_serv/proxy/
config.rs1use std::time::Duration;
4
5#[derive(Debug, Clone)]
7pub struct ProxyConfig {
8 pub path: String,
10 pub target: String,
12 pub strip_prefix: bool,
14 pub timeout: Duration,
16 pub preserve_host: bool,
18 pub follow_redirects: bool,
20}
21
22impl ProxyConfig {
23 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 pub fn with_strip_prefix(mut self, strip: bool) -> Self {
37 self.strip_prefix = strip;
38 self
39 }
40
41 pub fn with_timeout(mut self, timeout: Duration) -> Self {
43 self.timeout = timeout;
44 self
45 }
46
47 pub fn with_preserve_host(mut self, preserve: bool) -> Self {
49 self.preserve_host = preserve;
50 self
51 }
52
53 pub fn with_follow_redirects(mut self, follow: bool) -> Self {
55 self.follow_redirects = follow;
56 self
57 }
58
59 pub fn matches(&self, path: &str) -> bool {
61 path.starts_with(&self.path)
62 }
63
64 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 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 let target = self.target.trim_end_matches('/');
83 format!("{}{}", target, target_path)
84 }
85
86 pub fn parse_target(&self) -> Result<(String, String, Option<u16>), String> {
88 let target = &self.target;
89
90 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 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 }
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}