Skip to main content

meow_api/
lib.rs

1pub mod log_stream;
2pub mod routes;
3pub mod ui;
4
5use dashmap::DashMap;
6use log_stream::LogMessage;
7use meow_config::{
8    proxy_provider::ProxyProvider, raw::RawConfig, rule_provider::RuleProvider, NamedListener,
9};
10use meow_tunnel::Tunnel;
11use parking_lot::RwLock;
12use std::collections::HashMap;
13use std::net::SocketAddr;
14use std::path::PathBuf;
15use std::sync::Arc;
16use tokio::sync::broadcast;
17use tracing::{info, warn};
18
19/// How long to wait for the TUN readiness signal (device creation + stack
20/// init + child-task setup) before treating the listener as failed to
21/// start. Shared between the startup path (`meow-app/src/main.rs`) and the
22/// config-reload path (`routes.rs::spawn_tun_from_raw`) so the two don't
23/// drift.
24///
25/// Setup *failures* return immediately via `TunReady::Failed` — this bound
26/// only covers genuine hangs and legitimately slow startups: wintun adapter
27/// creation, first-time driver install, and the PowerShell DNS backup/set
28/// can together take minutes on slow Windows machines (5 s and 30 s both
29/// proved too aggressive there; 300 s measured comfortable in practice).
30/// Trade-off to be aware of: the config-reload path awaits this while
31/// holding `config_mutation_lock`, so a hung startup blocks every
32/// config-mutation API call for the full duration.
33pub const TUN_STARTUP_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(300);
34
35/// Map a parsed `TunConfig` onto a `TunListenerConfig`. Shared between the
36/// startup path (`meow-app/src/main.rs`) and the config-reload path
37/// (`routes.rs::spawn_tun_from_raw`) so the two don't drift.
38#[cfg(feature = "listener-tun")]
39pub fn tun_config_to_listener_config(
40    tun: &meow_config::TunConfig,
41) -> meow_listener::TunListenerConfig {
42    meow_listener::TunListenerConfig {
43        device: tun.device.clone(),
44        mtu: tun.mtu,
45        inet4_address: tun.inet4_address,
46        auto_route: tun.auto_route,
47        dns_hijack: tun.dns_hijack,
48        udp_timeout: tun.udp_timeout,
49    }
50}
51
52pub struct ApiServer {
53    tunnel: Tunnel,
54    listen_addr: SocketAddr,
55    secret: Option<String>,
56    config_path: String,
57    raw_config: Arc<RwLock<RawConfig>>,
58    log_tx: broadcast::Sender<LogMessage>,
59    proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
60    rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
61    listeners: Vec<NamedListener>,
62    external_ui: Option<PathBuf>,
63}
64
65impl ApiServer {
66    #[allow(clippy::too_many_arguments)]
67    pub fn new(
68        tunnel: Tunnel,
69        listen_addr: SocketAddr,
70        secret: Option<String>,
71        config_path: String,
72        raw_config: Arc<RwLock<RawConfig>>,
73        log_tx: broadcast::Sender<LogMessage>,
74        proxy_providers: Arc<DashMap<String, Arc<ProxyProvider>>>,
75        rule_providers: Arc<RwLock<HashMap<String, Arc<RuleProvider>>>>,
76        listeners: Vec<NamedListener>,
77        external_ui: Option<PathBuf>,
78    ) -> Self {
79        Self {
80            tunnel,
81            listen_addr,
82            secret,
83            config_path,
84            raw_config,
85            log_tx,
86            proxy_providers,
87            rule_providers,
88            listeners,
89            external_ui,
90        }
91    }
92
93    pub async fn run(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
94        let state = Arc::new(routes::AppState {
95            tunnel: self.tunnel.clone(),
96            secret: self.secret.clone(),
97            config_path: self.config_path.clone(),
98            raw_config: Arc::clone(&self.raw_config),
99            log_tx: self.log_tx.clone(),
100            proxy_providers: Arc::clone(&self.proxy_providers),
101            rule_providers: Arc::clone(&self.rule_providers),
102            listeners: self.listeners.clone(),
103            external_ui: self.resolve_external_ui(),
104            config_mutation_lock: tokio::sync::Mutex::new(()),
105        });
106
107        let app = routes::create_router(state);
108
109        let listener = tokio::net::TcpListener::bind(self.listen_addr).await?;
110        info!("REST API listening on {}", self.listen_addr);
111        info!("Web UI available at http://{}/ui", self.listen_addr);
112        axum::serve(listener, app).await?;
113        Ok(())
114    }
115
116    /// Validate the configured external-UI directory. Returns the path only when
117    /// it exists as a directory; otherwise logs a warning and falls back to the
118    /// built-in panel (issue #223).
119    fn resolve_external_ui(&self) -> Option<PathBuf> {
120        let dir = self.external_ui.as_ref()?;
121        if dir.is_dir() {
122            info!("Serving external Web UI from {}", dir.display());
123            Some(dir.clone())
124        } else {
125            warn!(
126                "external-ui directory {} not found; serving the built-in panel instead",
127                dir.display()
128            );
129            None
130        }
131    }
132}