1use crate::{Result, TunnelConfig};
4use async_trait::async_trait;
5use serde::{Deserialize, Serialize};
6use std::time::Duration;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TunnelStatus {
11 pub public_url: String,
13
14 pub tunnel_id: String,
16
17 pub active: bool,
19
20 pub request_count: u64,
22
23 pub bytes_transferred: u64,
25
26 pub created_at: Option<chrono::DateTime<chrono::Utc>>,
28
29 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
31
32 #[serde(skip_serializing_if = "Option::is_none")]
34 pub local_url: Option<String>,
35}
36
37#[async_trait]
39pub trait TunnelProvider: Send + Sync {
40 async fn create_tunnel(&self, config: &TunnelConfig) -> Result<TunnelStatus>;
42
43 async fn get_tunnel_status(&self, tunnel_id: &str) -> Result<TunnelStatus>;
45
46 async fn delete_tunnel(&self, tunnel_id: &str) -> Result<()>;
48
49 async fn list_tunnels(&self) -> Result<Vec<TunnelStatus>>;
51
52 async fn is_available(&self) -> bool;
54}
55
56pub struct SelfHostedProvider {
58 server_url: String,
59 auth_token: Option<String>,
60 client: reqwest::Client,
61}
62
63impl SelfHostedProvider {
64 pub fn new(server_url: impl Into<String>, auth_token: Option<String>) -> Self {
66 Self {
67 server_url: server_url.into(),
68 auth_token,
69 client: reqwest::Client::new(),
70 }
71 }
72}
73
74#[async_trait]
75impl TunnelProvider for SelfHostedProvider {
76 async fn create_tunnel(&self, config: &TunnelConfig) -> Result<TunnelStatus> {
77 let url = format!("{}/api/tunnels", self.server_url);
78 let mut request = self.client.post(&url);
79
80 if let Some(token) = &self.auth_token {
82 request = request.header("Authorization", format!("Bearer {}", token));
83 }
84
85 let payload = serde_json::json!({
86 "local_url": config.local_url,
87 "subdomain": config.subdomain,
88 "custom_domain": config.custom_domain,
89 "protocol": config.protocol,
90 "websocket_enabled": config.websocket_enabled,
91 "http2_enabled": config.http2_enabled,
92 });
93
94 let response = request
95 .json(&payload)
96 .send()
97 .await
98 .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
99
100 if !response.status().is_success() {
101 let error_text = response.text().await.unwrap_or_default();
102 return Err(crate::TunnelError::ProviderError(format!(
103 "Failed to create tunnel: {}",
104 error_text
105 )));
106 }
107
108 let status: TunnelStatus = response.json().await?;
109 Ok(status)
110 }
111
112 async fn get_tunnel_status(&self, tunnel_id: &str) -> Result<TunnelStatus> {
113 let url = format!("{}/api/tunnels/{}", self.server_url, tunnel_id);
114 let mut request = self.client.get(&url);
115
116 if let Some(token) = &self.auth_token {
117 request = request.header("Authorization", format!("Bearer {}", token));
118 }
119
120 let response = request
121 .send()
122 .await
123 .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
124
125 if !response.status().is_success() {
126 return Err(crate::TunnelError::NotFound(tunnel_id.to_string()));
127 }
128
129 let status: TunnelStatus = response.json().await?;
130 Ok(status)
131 }
132
133 async fn delete_tunnel(&self, tunnel_id: &str) -> Result<()> {
134 let url = format!("{}/api/tunnels/{}", self.server_url, tunnel_id);
135 let mut request = self.client.delete(&url);
136
137 if let Some(token) = &self.auth_token {
138 request = request.header("Authorization", format!("Bearer {}", token));
139 }
140
141 let response = request
142 .send()
143 .await
144 .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
145
146 if !response.status().is_success() {
147 return Err(crate::TunnelError::ProviderError(format!(
148 "Failed to delete tunnel: {}",
149 response.status()
150 )));
151 }
152
153 Ok(())
154 }
155
156 async fn list_tunnels(&self) -> Result<Vec<TunnelStatus>> {
157 let url = format!("{}/api/tunnels", self.server_url);
158 let mut request = self.client.get(&url);
159
160 if let Some(token) = &self.auth_token {
161 request = request.header("Authorization", format!("Bearer {}", token));
162 }
163
164 let response = request
165 .send()
166 .await
167 .map_err(|e| crate::TunnelError::ConnectionFailed(e.to_string()))?;
168
169 if !response.status().is_success() {
170 return Err(crate::TunnelError::ProviderError(format!(
171 "Failed to list tunnels: {}",
172 response.status()
173 )));
174 }
175
176 let tunnels: Vec<TunnelStatus> = response.json().await?;
177 Ok(tunnels)
178 }
179
180 async fn is_available(&self) -> bool {
181 let url = format!("{}/health", self.server_url);
182 self.client.get(&url).timeout(Duration::from_secs(5)).send().await.is_ok()
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use chrono::Utc;
190
191 fn create_test_status() -> TunnelStatus {
192 TunnelStatus {
193 public_url: "https://test.tunnel.dev".to_string(),
194 tunnel_id: "tunnel-123".to_string(),
195 active: true,
196 request_count: 100,
197 bytes_transferred: 5000,
198 created_at: Some(Utc::now()),
199 expires_at: None,
200 local_url: Some("http://localhost:3000".to_string()),
201 }
202 }
203
204 #[test]
205 fn test_tunnel_status_clone() {
206 let status = create_test_status();
207 let cloned = status.clone();
208 assert_eq!(status.public_url, cloned.public_url);
209 assert_eq!(status.tunnel_id, cloned.tunnel_id);
210 assert_eq!(status.active, cloned.active);
211 assert_eq!(status.request_count, cloned.request_count);
212 assert_eq!(status.bytes_transferred, cloned.bytes_transferred);
213 }
214
215 #[test]
216 fn test_tunnel_status_debug() {
217 let status = create_test_status();
218 let debug = format!("{:?}", status);
219 assert!(debug.contains("TunnelStatus"));
220 assert!(debug.contains("tunnel-123"));
221 }
222
223 #[test]
224 fn test_tunnel_status_serialize() {
225 let status = create_test_status();
226 let json = serde_json::to_string(&status).unwrap();
227 assert!(json.contains("\"public_url\":\"https://test.tunnel.dev\""));
228 assert!(json.contains("\"tunnel_id\":\"tunnel-123\""));
229 assert!(json.contains("\"active\":true"));
230 assert!(json.contains("\"request_count\":100"));
231 assert!(json.contains("\"bytes_transferred\":5000"));
232 }
233
234 #[test]
235 fn test_tunnel_status_deserialize() {
236 let json = r#"{
237 "public_url": "https://example.tunnel.dev",
238 "tunnel_id": "tun-456",
239 "active": false,
240 "request_count": 50,
241 "bytes_transferred": 2500,
242 "created_at": null,
243 "expires_at": null
244 }"#;
245
246 let status: TunnelStatus = serde_json::from_str(json).unwrap();
247 assert_eq!(status.public_url, "https://example.tunnel.dev");
248 assert_eq!(status.tunnel_id, "tun-456");
249 assert!(!status.active);
250 assert_eq!(status.request_count, 50);
251 assert_eq!(status.bytes_transferred, 2500);
252 assert!(status.created_at.is_none());
253 assert!(status.expires_at.is_none());
254 assert!(status.local_url.is_none());
255 }
256
257 #[test]
258 fn test_tunnel_status_serialize_skip_none_local_url() {
259 let status = TunnelStatus {
260 public_url: "https://test.tunnel.dev".to_string(),
261 tunnel_id: "tunnel-123".to_string(),
262 active: true,
263 request_count: 0,
264 bytes_transferred: 0,
265 created_at: None,
266 expires_at: None,
267 local_url: None,
268 };
269 let json = serde_json::to_string(&status).unwrap();
270 assert!(!json.contains("local_url"));
272 }
273
274 #[test]
275 fn test_tunnel_status_with_local_url() {
276 let status = TunnelStatus {
277 public_url: "https://test.tunnel.dev".to_string(),
278 tunnel_id: "tunnel-123".to_string(),
279 active: true,
280 request_count: 0,
281 bytes_transferred: 0,
282 created_at: None,
283 expires_at: None,
284 local_url: Some("http://localhost:8080".to_string()),
285 };
286 let json = serde_json::to_string(&status).unwrap();
287 assert!(json.contains("\"local_url\":\"http://localhost:8080\""));
288 }
289
290 #[test]
291 fn test_tunnel_status_with_timestamps() {
292 let now = Utc::now();
293 let expires = now + chrono::Duration::hours(24);
294
295 let status = TunnelStatus {
296 public_url: "https://test.tunnel.dev".to_string(),
297 tunnel_id: "tunnel-123".to_string(),
298 active: true,
299 request_count: 0,
300 bytes_transferred: 0,
301 created_at: Some(now),
302 expires_at: Some(expires),
303 local_url: None,
304 };
305
306 assert!(status.created_at.is_some());
307 assert!(status.expires_at.is_some());
308 assert!(status.expires_at.unwrap() > status.created_at.unwrap());
309 }
310
311 #[test]
312 fn test_self_hosted_provider_new() {
313 let provider = SelfHostedProvider::new("https://tunnel.example.com", None);
314 assert_eq!(provider.server_url, "https://tunnel.example.com");
315 assert!(provider.auth_token.is_none());
316 }
317
318 #[test]
319 fn test_self_hosted_provider_new_with_token() {
320 let provider = SelfHostedProvider::new(
321 "https://tunnel.example.com",
322 Some("my-secret-token".to_string()),
323 );
324 assert_eq!(provider.server_url, "https://tunnel.example.com");
325 assert_eq!(provider.auth_token, Some("my-secret-token".to_string()));
326 }
327
328 #[test]
329 fn test_self_hosted_provider_new_with_string_conversion() {
330 let provider = SelfHostedProvider::new(String::from("https://api.tunnel.dev"), None);
331 assert_eq!(provider.server_url, "https://api.tunnel.dev");
332 }
333
334 #[test]
335 fn test_tunnel_status_roundtrip_serialization() {
336 let status = create_test_status();
337 let json = serde_json::to_string(&status).unwrap();
338 let deserialized: TunnelStatus = serde_json::from_str(&json).unwrap();
339
340 assert_eq!(status.public_url, deserialized.public_url);
341 assert_eq!(status.tunnel_id, deserialized.tunnel_id);
342 assert_eq!(status.active, deserialized.active);
343 assert_eq!(status.request_count, deserialized.request_count);
344 assert_eq!(status.bytes_transferred, deserialized.bytes_transferred);
345 assert_eq!(status.local_url, deserialized.local_url);
346 }
347
348 #[test]
349 fn test_tunnel_status_inactive() {
350 let status = TunnelStatus {
351 public_url: String::new(),
352 tunnel_id: "inactive-tunnel".to_string(),
353 active: false,
354 request_count: 0,
355 bytes_transferred: 0,
356 created_at: None,
357 expires_at: None,
358 local_url: None,
359 };
360
361 assert!(!status.active);
362 assert!(status.public_url.is_empty());
363 }
364
365 #[test]
366 fn test_tunnel_status_high_traffic() {
367 let status = TunnelStatus {
368 public_url: "https://high-traffic.tunnel.dev".to_string(),
369 tunnel_id: "high-traffic-1".to_string(),
370 active: true,
371 request_count: u64::MAX,
372 bytes_transferred: u64::MAX,
373 created_at: Some(Utc::now()),
374 expires_at: None,
375 local_url: None,
376 };
377
378 assert_eq!(status.request_count, u64::MAX);
379 assert_eq!(status.bytes_transferred, u64::MAX);
380 }
381}