mockforge_tunnel/
client.rs

1//! Tunnel client for forwarding requests
2
3use crate::{manager::TunnelManager, Result};
4use axum::{body::Body, extract::Request, response::Response};
5use std::sync::Arc;
6
7/// Tunnel client for forwarding HTTP requests
8pub struct TunnelClient {
9    #[allow(dead_code)] // Reserved for future use
10    manager: Arc<TunnelManager>,
11    local_url: String,
12}
13
14impl TunnelClient {
15    /// Create a new tunnel client
16    pub fn new(manager: Arc<TunnelManager>, local_url: impl Into<String>) -> Self {
17        Self {
18            manager,
19            local_url: local_url.into(),
20        }
21    }
22
23    /// Forward an HTTP request to the local server
24    pub async fn forward_request(&self, request: Request) -> Result<Response> {
25        let uri = request.uri().clone();
26        let method = request.method().clone();
27        let headers = request.headers().clone();
28
29        // Build the local URL
30        let local_uri = format!(
31            "{}{}",
32            self.local_url,
33            uri.path_and_query().map(|pq| pq.as_str()).unwrap_or("")
34        );
35
36        // Create new request to local server
37        let mut local_request = reqwest::Client::new().request(method, &local_uri);
38
39        // Copy headers (excluding hop-by-hop headers)
40        for (name, value) in headers.iter() {
41            let name_str = name.as_str();
42            if !matches!(
43                name_str,
44                "connection"
45                    | "keep-alive"
46                    | "proxy-authenticate"
47                    | "proxy-authorization"
48                    | "te"
49                    | "trailer"
50                    | "transfer-encoding"
51                    | "upgrade"
52            ) {
53                if let Ok(value_str) = value.to_str() {
54                    local_request = local_request.header(name_str, value_str);
55                }
56            }
57        }
58
59        // Set Host header to local
60        if let Ok(host) = url::Url::parse(&self.local_url) {
61            if let Some(host_str) = host.host_str() {
62                local_request = local_request.header("Host", host_str);
63            }
64        }
65
66        // Forward request body if present
67        let body_bytes =
68            axum::body::to_bytes(request.into_body(), usize::MAX).await.map_err(|e| {
69                crate::TunnelError::Io(std::io::Error::other(format!(
70                    "Failed to read request body: {}",
71                    e
72                )))
73            })?;
74
75        if !body_bytes.is_empty() {
76            local_request = local_request.body(body_bytes.to_vec());
77        }
78
79        // Send request to local server
80        let response = local_request
81            .send()
82            .await
83            .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
84
85        // Build response
86        let status = response.status();
87        let headers = response.headers().clone();
88        let body = response.bytes().await.map_err(|e| {
89            crate::TunnelError::ConnectionFailed(format!("Failed to read response: {}", e))
90        })?;
91
92        let mut response_builder = Response::builder().status(status);
93
94        // Copy response headers
95        for (name, value) in headers.iter() {
96            if let Ok(value_str) = value.to_str() {
97                response_builder = response_builder.header(name.as_str(), value_str);
98            }
99        }
100
101        response_builder.body(Body::from(body.to_vec())).map_err(|e| {
102            crate::TunnelError::ConnectionFailed(format!("Failed to build response: {}", e))
103        })
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110    use crate::config::TunnelConfig;
111    use std::sync::Arc;
112
113    fn create_test_manager() -> Arc<TunnelManager> {
114        let config = TunnelConfig {
115            provider: crate::config::TunnelProvider::SelfHosted,
116            server_url: Some("https://tunnel.example.com".to_string()),
117            auth_token: Some("test-token".to_string()),
118            subdomain: Some("test".to_string()),
119            local_url: "http://localhost:3000".to_string(),
120            protocol: "http".to_string(),
121            region: None,
122            custom_domain: None,
123            websocket_enabled: true,
124            http2_enabled: true,
125        };
126
127        Arc::new(TunnelManager::new(&config).unwrap())
128    }
129
130    #[test]
131    fn test_tunnel_client_new() {
132        let manager = create_test_manager();
133        let client = TunnelClient::new(manager.clone(), "http://localhost:3000");
134
135        assert_eq!(client.local_url, "http://localhost:3000");
136    }
137
138    #[test]
139    fn test_tunnel_client_new_with_string() {
140        let manager = create_test_manager();
141        let url = String::from("http://127.0.0.1:8080");
142        let client = TunnelClient::new(manager.clone(), url.clone());
143
144        assert_eq!(client.local_url, url);
145    }
146
147    #[test]
148    fn test_tunnel_client_new_with_different_urls() {
149        let manager = create_test_manager();
150
151        let urls = vec![
152            "http://localhost:3000",
153            "http://127.0.0.1:8080",
154            "http://0.0.0.0:5000",
155            "https://internal-api:443",
156            "http://[::1]:9000",
157        ];
158
159        for url in urls {
160            let client = TunnelClient::new(manager.clone(), url);
161            assert_eq!(client.local_url, url, "URL mismatch for {}", url);
162        }
163    }
164
165    #[test]
166    fn test_tunnel_client_new_with_https() {
167        let manager = create_test_manager();
168        let client = TunnelClient::new(manager.clone(), "https://localhost:8443");
169
170        assert_eq!(client.local_url, "https://localhost:8443");
171    }
172
173    #[test]
174    fn test_tunnel_client_new_with_custom_port() {
175        let manager = create_test_manager();
176        let client = TunnelClient::new(manager.clone(), "http://localhost:4040");
177
178        assert_eq!(client.local_url, "http://localhost:4040");
179    }
180
181    #[test]
182    fn test_tunnel_client_local_url_formatting() {
183        let manager = create_test_manager();
184
185        // Test various URL formats
186        let test_cases = vec![
187            ("http://localhost:3000", "http://localhost:3000"),
188            ("http://localhost:3000/", "http://localhost:3000/"),
189            ("http://api.local:8080", "http://api.local:8080"),
190        ];
191
192        for (input, expected) in test_cases {
193            let client = TunnelClient::new(manager.clone(), input);
194            assert_eq!(client.local_url, expected);
195        }
196    }
197
198    #[test]
199    fn test_tunnel_client_manager_reference() {
200        let manager = create_test_manager();
201        let manager_clone = manager.clone();
202
203        let _client = TunnelClient::new(manager, "http://localhost:3000");
204
205        // Original manager should still be accessible via clone
206        assert!(Arc::strong_count(&manager_clone) >= 1);
207    }
208
209    #[test]
210    fn test_tunnel_client_creation_multiple() {
211        let manager = create_test_manager();
212
213        let client1 = TunnelClient::new(manager.clone(), "http://localhost:3000");
214        let client2 = TunnelClient::new(manager.clone(), "http://localhost:4000");
215
216        assert_eq!(client1.local_url, "http://localhost:3000");
217        assert_eq!(client2.local_url, "http://localhost:4000");
218    }
219
220    #[test]
221    fn test_tunnel_client_into_conversion() {
222        let manager = create_test_manager();
223
224        // Test that Into<String> trait works for various types
225        let url_str = "http://localhost:3000";
226        let url_string = String::from("http://localhost:3000");
227
228        let client1 = TunnelClient::new(manager.clone(), url_str);
229        let client2 = TunnelClient::new(manager.clone(), url_string);
230
231        assert_eq!(client1.local_url, client2.local_url);
232    }
233
234    #[test]
235    fn test_tunnel_client_with_ipv6() {
236        let manager = create_test_manager();
237        let client = TunnelClient::new(manager, "http://[::1]:3000");
238
239        assert_eq!(client.local_url, "http://[::1]:3000");
240    }
241
242    #[test]
243    fn test_tunnel_client_with_hostname() {
244        let manager = create_test_manager();
245        let client = TunnelClient::new(manager, "http://backend-service:8080");
246
247        assert_eq!(client.local_url, "http://backend-service:8080");
248    }
249
250    #[test]
251    fn test_tunnel_client_url_without_port() {
252        let manager = create_test_manager();
253
254        // HTTP default port
255        let client_http = TunnelClient::new(manager.clone(), "http://localhost");
256        assert_eq!(client_http.local_url, "http://localhost");
257
258        // HTTPS default port
259        let client_https = TunnelClient::new(manager.clone(), "https://localhost");
260        assert_eq!(client_https.local_url, "https://localhost");
261    }
262
263    #[test]
264    fn test_tunnel_client_empty_url() {
265        let manager = create_test_manager();
266        let client = TunnelClient::new(manager, "");
267
268        assert_eq!(client.local_url, "");
269    }
270
271    #[test]
272    fn test_tunnel_client_with_path() {
273        let manager = create_test_manager();
274        let client = TunnelClient::new(manager, "http://localhost:3000/api");
275
276        // Path should be preserved in local_url
277        assert_eq!(client.local_url, "http://localhost:3000/api");
278    }
279
280    #[test]
281    fn test_tunnel_client_with_query_params() {
282        let manager = create_test_manager();
283        let client = TunnelClient::new(manager, "http://localhost:3000?debug=true");
284
285        assert_eq!(client.local_url, "http://localhost:3000?debug=true");
286    }
287}