Skip to main content

ciab_gateway/
lib.rs

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
27/// Top-level coordinator for the gateway subsystem.
28pub 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        // Pick the active tunnel manager based on the configured provider.
40        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            // Legacy: if no tunnel_provider set but frp is enabled, use frp
54            _ 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    /// Collect provider info for all known providers.
126    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        // Add info for providers not currently active
134        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    /// Prepare (download/install/validate) a tunnel provider.
163    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    // --- Token operations ---
190
191    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    // --- Tunnel operations ---
261
262    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        // Try to stop via the tunnel manager if it matches the active provider
312        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        // For LAN tunnels, compute the public URL from local addresses.
356        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/// Result of the convenience `expose` operation.
384#[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
393// -------------------------------------------------------------------------
394// Row <-> Type conversion helpers
395// -------------------------------------------------------------------------
396
397use 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}