1pub mod lan;
2pub mod proxy;
3pub mod tokens;
4pub mod tunnel;
5pub mod types;
6
7use std::sync::Arc;
8
9use chrono::Utc;
10use ciab_core::error::{CiabError, CiabResult};
11use ciab_core::types::config::GatewayConfig;
12use ciab_db::Database;
13use uuid::Uuid;
14
15use crate::lan::LanDiscovery;
16use crate::tokens::{generate_token, hash_token};
17use crate::tunnel::bore::BoreTunnelManager;
18use crate::tunnel::cloudflare::CloudflareTunnelManager;
19use crate::tunnel::frp::FrpTunnelManager;
20use crate::tunnel::ngrok::NgrokTunnelManager;
21use crate::tunnel::TunnelManager;
22use crate::types::{
23 ClientToken, FrpStatus, GatewayStatus, GatewayTunnel, ProviderPrepareResult, TokenScope,
24 TunnelProviderInfo, TunnelState, TunnelType,
25};
26
27pub struct GatewayManager {
29 pub config: GatewayConfig,
30 pub db: Arc<Database>,
31 pub lan: LanDiscovery,
32 tunnel_manager: Option<Box<dyn TunnelManager>>,
33}
34
35impl GatewayManager {
36 pub fn new(config: GatewayConfig, db: Arc<Database>) -> Self {
37 let lan = LanDiscovery::new(config.lan.clone());
38
39 let tunnel_manager: Option<Box<dyn TunnelManager>> = match config.tunnel_provider.as_str() {
41 "frp" if config.frp.enabled => {
42 Some(Box::new(FrpTunnelManager::new(config.frp.clone())))
43 }
44 "bore" if config.bore.enabled => {
45 Some(Box::new(BoreTunnelManager::new(config.bore.clone())))
46 }
47 "cloudflare" if config.cloudflare.enabled => Some(Box::new(
48 CloudflareTunnelManager::new(config.cloudflare.clone()),
49 )),
50 "ngrok" if config.ngrok.enabled => {
51 Some(Box::new(NgrokTunnelManager::new(config.ngrok.clone())))
52 }
53 _ if config.frp.enabled => Some(Box::new(FrpTunnelManager::new(config.frp.clone()))),
55 _ => None,
56 };
57
58 Self {
59 config,
60 db,
61 lan,
62 tunnel_manager,
63 }
64 }
65
66 pub async fn start(&self) -> CiabResult<()> {
67 if !self.config.enabled {
68 tracing::info!("Gateway subsystem disabled");
69 return Ok(());
70 }
71
72 self.lan.start().await?;
73 tracing::info!("Gateway subsystem started");
74 Ok(())
75 }
76
77 pub async fn shutdown(&self) -> CiabResult<()> {
78 self.lan.stop().await?;
79 if let Some(ref tm) = self.tunnel_manager {
80 tm.shutdown().await?;
81 }
82 Ok(())
83 }
84
85 pub async fn status(&self) -> CiabResult<GatewayStatus> {
86 let tunnel_rows = self.db.list_gateway_tunnel_rows().await?;
87 let active_tunnels = tunnel_rows.iter().filter(|t| t.state == "active").count();
88 let token_rows = self.db.list_client_token_rows().await?;
89 let active_tokens = token_rows.iter().filter(|t| t.revoked_at.is_none()).count();
90
91 let frp_status = FrpStatus {
92 enabled: self.config.frp.enabled,
93 process_running: self
94 .tunnel_manager
95 .as_ref()
96 .map(|tm| tm.is_running())
97 .unwrap_or(false),
98 server_addr: self.config.frp.server_addr.clone(),
99 proxy_count: if let Some(ref tm) = self.tunnel_manager {
100 tm.list_tunnels().await?.len()
101 } else {
102 0
103 },
104 };
105
106 let active_provider = self
107 .tunnel_manager
108 .as_ref()
109 .map(|tm| tm.provider_name().to_string())
110 .unwrap_or_else(|| self.config.tunnel_provider.clone());
111
112 let providers = self.collect_provider_infos();
113
114 Ok(GatewayStatus {
115 enabled: self.config.enabled,
116 active_provider,
117 lan: self.lan.status(),
118 providers,
119 frp: frp_status,
120 active_tunnels,
121 active_tokens,
122 })
123 }
124
125 fn collect_provider_infos(&self) -> Vec<TunnelProviderInfo> {
127 let mut infos = Vec::new();
128
129 if let Some(ref tm) = self.tunnel_manager {
130 infos.push(tm.info());
131 }
132
133 let active_name = self
135 .tunnel_manager
136 .as_ref()
137 .map(|tm| tm.provider_name().to_string())
138 .unwrap_or_default();
139
140 for (name, enabled) in [
141 ("frp", self.config.frp.enabled),
142 ("bore", self.config.bore.enabled),
143 ("cloudflare", self.config.cloudflare.enabled),
144 ("ngrok", self.config.ngrok.enabled),
145 ] {
146 if name != active_name {
147 infos.push(TunnelProviderInfo {
148 name: name.to_string(),
149 enabled,
150 installed: false,
151 binary_path: None,
152 version: None,
153 process_running: false,
154 tunnel_count: 0,
155 });
156 }
157 }
158
159 infos
160 }
161
162 pub async fn prepare_provider(&self, provider: &str) -> CiabResult<ProviderPrepareResult> {
164 let (binary, auto_install) = match provider {
165 "bore" => (
166 self.config.bore.binary.as_str(),
167 self.config.bore.auto_install,
168 ),
169 "cloudflare" => (
170 self.config.cloudflare.binary.as_str(),
171 self.config.cloudflare.auto_install,
172 ),
173 "ngrok" => (
174 self.config.ngrok.binary.as_str(),
175 self.config.ngrok.auto_install,
176 ),
177 "frp" => (self.config.frp.frpc_binary.as_str(), false),
178 other => {
179 return Err(CiabError::TunnelProviderError(format!(
180 "Unknown tunnel provider: {}",
181 other
182 )));
183 }
184 };
185
186 tunnel::provider::prepare_provider(provider, binary, auto_install).await
187 }
188
189 pub async fn create_token(
192 &self,
193 name: String,
194 scopes: Vec<TokenScope>,
195 expires_secs: Option<u64>,
196 ) -> CiabResult<(String, ClientToken)> {
197 let raw_token = generate_token();
198 let token_hash = hash_token(&raw_token);
199 let now = Utc::now();
200 let expires_at = expires_secs.map(|s| now + chrono::Duration::seconds(s as i64));
201
202 let token = ClientToken {
203 id: Uuid::new_v4(),
204 name,
205 token_hash,
206 scopes,
207 expires_at,
208 last_used_at: None,
209 created_at: now,
210 revoked_at: None,
211 };
212
213 let row = token_to_row(&token)?;
214 self.db.insert_client_token_row(&row).await?;
215
216 Ok((raw_token, token))
217 }
218
219 pub async fn validate_token(&self, raw_token: &str) -> CiabResult<ClientToken> {
220 let hash = hash_token(raw_token);
221 let row = self.db.get_client_token_row_by_hash(&hash).await?.ok_or(
222 CiabError::ClientTokenNotFound("token not found".to_string()),
223 )?;
224
225 let token = row_to_token(&row)?;
226
227 if token.revoked_at.is_some() {
228 return Err(CiabError::ClientTokenRevoked);
229 }
230
231 if let Some(expires_at) = token.expires_at {
232 if Utc::now() > expires_at {
233 return Err(CiabError::ClientTokenExpired);
234 }
235 }
236
237 let _ = self.db.touch_client_token_row(&row.id).await;
238
239 Ok(token)
240 }
241
242 pub async fn revoke_token(&self, token_id: &Uuid) -> CiabResult<()> {
243 self.db.revoke_client_token_row(&token_id.to_string()).await
244 }
245
246 pub async fn list_tokens(&self) -> CiabResult<Vec<ClientToken>> {
247 let rows = self.db.list_client_token_rows().await?;
248 rows.into_iter().map(|r| row_to_token(&r)).collect()
249 }
250
251 pub async fn get_token(&self, token_id: &Uuid) -> CiabResult<ClientToken> {
252 let row = self
253 .db
254 .get_client_token_row(&token_id.to_string())
255 .await?
256 .ok_or_else(|| CiabError::ClientTokenNotFound(token_id.to_string()))?;
257 row_to_token(&row)
258 }
259
260 pub async fn create_tunnel(
263 &self,
264 sandbox_id: Option<Uuid>,
265 tunnel_type: TunnelType,
266 local_port: u16,
267 public_url: Option<String>,
268 ) -> CiabResult<GatewayTunnel> {
269 let tunnel = match tunnel_type {
270 TunnelType::Frp | TunnelType::Bore | TunnelType::Cloudflare | TunnelType::Ngrok => {
271 let tm = self
272 .tunnel_manager
273 .as_ref()
274 .ok_or(CiabError::GatewayNotEnabled)?;
275 tm.create_tunnel(sandbox_id, local_port).await?
276 }
277 TunnelType::Manual | TunnelType::Lan => {
278 let url = public_url.ok_or_else(|| {
279 CiabError::TunnelCreationFailed(
280 "public_url required for manual/lan tunnel".to_string(),
281 )
282 })?;
283 let now = Utc::now();
284 GatewayTunnel {
285 id: Uuid::new_v4(),
286 sandbox_id,
287 tunnel_type,
288 public_url: url,
289 local_port,
290 state: TunnelState::Active,
291 config_json: serde_json::json!({}),
292 error_message: None,
293 created_at: now,
294 updated_at: now,
295 }
296 }
297 };
298
299 let row = tunnel_to_row(&tunnel)?;
300 self.db.insert_gateway_tunnel_row(&row).await?;
301 Ok(tunnel)
302 }
303
304 pub async fn stop_tunnel(&self, tunnel_id: &Uuid) -> CiabResult<()> {
305 let row = self
306 .db
307 .get_gateway_tunnel_row(&tunnel_id.to_string())
308 .await?
309 .ok_or_else(|| CiabError::TunnelNotFound(tunnel_id.to_string()))?;
310
311 let provider_types = ["frp", "bore", "cloudflare", "ngrok"];
313 if provider_types.contains(&row.tunnel_type.as_str()) {
314 if let Some(ref tm) = self.tunnel_manager {
315 let _ = tm.stop_tunnel(tunnel_id).await;
316 }
317 }
318
319 self.db
320 .delete_gateway_tunnel_row(&tunnel_id.to_string())
321 .await
322 }
323
324 pub async fn list_tunnels(&self) -> CiabResult<Vec<GatewayTunnel>> {
325 let rows = self.db.list_gateway_tunnel_rows().await?;
326 rows.into_iter().map(|r| row_to_tunnel(&r)).collect()
327 }
328
329 pub async fn get_tunnel(&self, tunnel_id: &Uuid) -> CiabResult<GatewayTunnel> {
330 let row = self
331 .db
332 .get_gateway_tunnel_row(&tunnel_id.to_string())
333 .await?
334 .ok_or_else(|| CiabError::TunnelNotFound(tunnel_id.to_string()))?;
335 row_to_tunnel(&row)
336 }
337
338 pub async fn expose_sandbox(
339 &self,
340 sandbox_id: Uuid,
341 token_name: Option<String>,
342 expires_secs: Option<u64>,
343 scope: Option<TokenScope>,
344 local_port: u16,
345 ) -> CiabResult<ExposeResult> {
346 let tunnel_type = if self.tunnel_manager.is_some() {
347 self.config
348 .tunnel_provider
349 .parse::<TunnelType>()
350 .unwrap_or(TunnelType::Lan)
351 } else {
352 TunnelType::Lan
353 };
354
355 let public_url = if tunnel_type == TunnelType::Lan {
357 let addrs = self.lan.status().local_addresses;
358 let addr = addrs
359 .first()
360 .cloned()
361 .unwrap_or_else(|| "127.0.0.1".to_string());
362 Some(format!("http://{}:{}", addr, local_port))
363 } else {
364 None
365 };
366
367 let tunnel = self
368 .create_tunnel(Some(sandbox_id), tunnel_type, local_port, public_url)
369 .await?;
370
371 let name = token_name.unwrap_or_else(|| format!("expose-{}", &sandbox_id.to_string()[..8]));
372 let scopes = vec![scope.unwrap_or(TokenScope::SandboxAccess { sandbox_id })];
373 let (raw_token, token) = self.create_token(name, scopes, expires_secs).await?;
374
375 Ok(ExposeResult {
376 tunnel,
377 raw_token,
378 token,
379 })
380 }
381}
382
383#[derive(Debug, Clone, serde::Serialize)]
385pub struct ExposeResult {
386 pub tunnel: GatewayTunnel,
387 #[serde(rename = "token")]
388 pub raw_token: String,
389 #[serde(rename = "token_info")]
390 pub token: ClientToken,
391}
392
393use ciab_db::{ClientTokenRow, GatewayTunnelRow};
398
399fn tunnel_to_row(t: &GatewayTunnel) -> CiabResult<GatewayTunnelRow> {
400 Ok(GatewayTunnelRow {
401 id: t.id.to_string(),
402 sandbox_id: t.sandbox_id.map(|id| id.to_string()),
403 tunnel_type: t.tunnel_type.to_string(),
404 public_url: t.public_url.clone(),
405 local_port: t.local_port as i64,
406 state: t.state.to_string(),
407 config_json: serde_json::to_string(&t.config_json)?,
408 error_message: t.error_message.clone(),
409 created_at: t.created_at.to_rfc3339(),
410 updated_at: t.updated_at.to_rfc3339(),
411 })
412}
413
414fn row_to_tunnel(r: &GatewayTunnelRow) -> CiabResult<GatewayTunnel> {
415 Ok(GatewayTunnel {
416 id: Uuid::parse_str(&r.id).map_err(|e| CiabError::Database(e.to_string()))?,
417 sandbox_id: r
418 .sandbox_id
419 .as_ref()
420 .map(|s| Uuid::parse_str(s))
421 .transpose()
422 .map_err(|e| CiabError::Database(e.to_string()))?,
423 tunnel_type: r
424 .tunnel_type
425 .parse()
426 .map_err(|e: String| CiabError::Database(e))?,
427 public_url: r.public_url.clone(),
428 local_port: r.local_port as u16,
429 state: r
430 .state
431 .parse()
432 .map_err(|e: String| CiabError::Database(e))?,
433 config_json: serde_json::from_str(&r.config_json)?,
434 error_message: r.error_message.clone(),
435 created_at: chrono::DateTime::parse_from_rfc3339(&r.created_at)
436 .map_err(|e| CiabError::Database(e.to_string()))?
437 .with_timezone(&Utc),
438 updated_at: chrono::DateTime::parse_from_rfc3339(&r.updated_at)
439 .map_err(|e| CiabError::Database(e.to_string()))?
440 .with_timezone(&Utc),
441 })
442}
443
444fn token_to_row(t: &ClientToken) -> CiabResult<ClientTokenRow> {
445 Ok(ClientTokenRow {
446 id: t.id.to_string(),
447 name: t.name.clone(),
448 token_hash: t.token_hash.clone(),
449 scopes_json: serde_json::to_string(&t.scopes)?,
450 expires_at: t.expires_at.map(|d| d.to_rfc3339()),
451 last_used_at: t.last_used_at.map(|d| d.to_rfc3339()),
452 created_at: t.created_at.to_rfc3339(),
453 revoked_at: t.revoked_at.map(|d| d.to_rfc3339()),
454 })
455}
456
457fn row_to_token(r: &ClientTokenRow) -> CiabResult<ClientToken> {
458 Ok(ClientToken {
459 id: Uuid::parse_str(&r.id).map_err(|e| CiabError::Database(e.to_string()))?,
460 name: r.name.clone(),
461 token_hash: r.token_hash.clone(),
462 scopes: serde_json::from_str(&r.scopes_json)?,
463 expires_at: r
464 .expires_at
465 .as_ref()
466 .map(|s| chrono::DateTime::parse_from_rfc3339(s).map(|d| d.with_timezone(&Utc)))
467 .transpose()
468 .map_err(|e| CiabError::Database(e.to_string()))?,
469 last_used_at: r
470 .last_used_at
471 .as_ref()
472 .map(|s| chrono::DateTime::parse_from_rfc3339(s).map(|d| d.with_timezone(&Utc)))
473 .transpose()
474 .map_err(|e| CiabError::Database(e.to_string()))?,
475 created_at: chrono::DateTime::parse_from_rfc3339(&r.created_at)
476 .map_err(|e| CiabError::Database(e.to_string()))?
477 .with_timezone(&Utc),
478 revoked_at: r
479 .revoked_at
480 .as_ref()
481 .map(|s| chrono::DateTime::parse_from_rfc3339(s).map(|d| d.with_timezone(&Utc)))
482 .transpose()
483 .map_err(|e| CiabError::Database(e.to_string()))?,
484 })
485}